Merge "Enabling room variants now that they should work" 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-lint/src/test/java/androidx/activity/lint/ActivityResultFragmentVersionDetectorTest.kt b/activity/activity-lint/src/test/java/androidx/activity/lint/ActivityResultFragmentVersionDetectorTest.kt
index 29924f1..170ebcbe 100644
--- a/activity/activity-lint/src/test/java/androidx/activity/lint/ActivityResultFragmentVersionDetectorTest.kt
+++ b/activity/activity-lint/src/test/java/androidx/activity/lint/ActivityResultFragmentVersionDetectorTest.kt
@@ -17,6 +17,7 @@
package androidx.activity.lint
import androidx.activity.lint.ActivityResultFragmentVersionDetector.Companion.FRAGMENT_VERSION
+import androidx.activity.lint.stubs.STUBS
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
@@ -45,6 +46,7 @@
val launcher = ActivityResultCaller().registerForActivityResult(ActivityResultContract())
"""
),
+ *STUBS,
gradle(
"build.gradle",
"""
@@ -70,6 +72,7 @@
val launcher = ActivityResultCaller().registerForActivityResult(ActivityResultContract())
"""
),
+ *STUBS,
gradle(
"build.gradle",
"""
@@ -95,6 +98,7 @@
val launcher = ActivityResultCaller().registerForActivityResult(ActivityResultContract())
"""
),
+ *STUBS,
gradle(
"build.gradle",
"""
@@ -119,6 +123,7 @@
val launcher = ActivityResultCaller().registerForActivityResult(ActivityResultContract())
"""
),
+ *STUBS,
gradle(
"build.gradle",
"""
@@ -143,6 +148,7 @@
val launcher = ActivityResultCaller().registerForActivityResult(ActivityResultContract())
"""
),
+ *STUBS,
gradle(
"build.gradle",
"""
@@ -167,6 +173,7 @@
val launcher = ActivityResultCaller().registerForActivityResult(ActivityResultContract())
"""
),
+ *STUBS,
gradle(
"build.gradle",
"""
@@ -189,6 +196,7 @@
}
"""
),
+ *STUBS,
kotlin(
"""
package com.example
@@ -221,6 +229,7 @@
}
"""
),
+ *STUBS,
kotlin(
"""
package com.example
@@ -257,6 +266,7 @@
}
"""
),
+ *STUBS,
kotlin(
"""
package com.example
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/appcompat/appcompat-lint/src/test/kotlin/androidx/appcompat/lint/Stubs.kt b/appcompat/appcompat-lint/src/test/kotlin/androidx/appcompat/lint/Stubs.kt
index d7aae48..beeedb2 100644
--- a/appcompat/appcompat-lint/src/test/kotlin/androidx/appcompat/lint/Stubs.kt
+++ b/appcompat/appcompat-lint/src/test/kotlin/androidx/appcompat/lint/Stubs.kt
@@ -49,6 +49,19 @@
)
.indented().within("src")
+ val CONTEXT_COMPAT: TestFile = LintDetectorTest.java(
+ "androidx/core/content/ContextCompat.java",
+ """
+ package androidx.core.content;
+ public class ContextCompat {
+ protected ContextCompat() {}
+ public static Drawable getDrawable(@NonNull Context context, @DrawableRes int id) {
+ throw new Exception();
+ }
+ }
+ """
+ ).indented().within("src")
+
val COLOR_STATE_LIST: TestFile = xml(
"color/color_state_list.xml",
"""
diff --git a/appcompat/appcompat-lint/src/test/kotlin/androidx/appcompat/lint/res/DrawableLoadingDetectorTest.kt b/appcompat/appcompat-lint/src/test/kotlin/androidx/appcompat/lint/res/DrawableLoadingDetectorTest.kt
index bc688c5..99f0ac0 100644
--- a/appcompat/appcompat-lint/src/test/kotlin/androidx/appcompat/lint/res/DrawableLoadingDetectorTest.kt
+++ b/appcompat/appcompat-lint/src/test/kotlin/androidx/appcompat/lint/res/DrawableLoadingDetectorTest.kt
@@ -52,6 +52,7 @@
// is on our own custom inner class
lint().files(
Stubs.APPCOMPAT_ACTIVITY,
+ Stubs.CONTEXT_COMPAT,
customActivity
).issues(DrawableLoadingDetector.NOT_USING_COMPAT_LOADING)
.run()
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextEmojiTest.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextEmojiTest.java
index 5f6eff5..268f8e5 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextEmojiTest.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextEmojiTest.java
@@ -18,6 +18,9 @@
import static com.google.common.truth.Truth.assertThat;
+import android.text.method.KeyListener;
+import android.view.KeyEvent;
+
import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -48,4 +51,33 @@
assertThat(notFocusable.isEnabled()).isFalse();
assertThat(notFocusable.isFocusable()).isFalse();
}
+
+ @Test
+ @UiThreadTest
+ public void respectsDigits() {
+ AppCompatEditText textWithDigits = mActivityTestRule.getActivity()
+ .findViewById(androidx.appcompat.test.R.id.text_with_digits);
+
+ int[] acceptedKeyCodes = {KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_2,
+ KeyEvent.KEYCODE_3, KeyEvent.KEYCODE_4};
+ int[] disallowedKeyCodes = {KeyEvent.KEYCODE_5, KeyEvent.KEYCODE_6, KeyEvent.KEYCODE_7,
+ KeyEvent.KEYCODE_8, KeyEvent.KEYCODE_9};
+ int[] actions = {KeyEvent.ACTION_DOWN, KeyEvent.ACTION_UP};
+
+ for (int action : actions) {
+ for (int keycode : acceptedKeyCodes) {
+ assertThat(listenerHandlesKeyEvent(textWithDigits, action, keycode)).isTrue();
+ }
+ for (int keycode : disallowedKeyCodes) {
+ assertThat(listenerHandlesKeyEvent(textWithDigits, action, keycode)).isFalse();
+ }
+ }
+ }
+
+ private boolean listenerHandlesKeyEvent(AppCompatEditText textWithDigits, int action,
+ int keycode) {
+ KeyListener listener = textWithDigits.getKeyListener();
+ return listener.onKeyDown(textWithDigits, textWithDigits.getText(),
+ keycode, new KeyEvent(action, keycode));
+ }
}
diff --git a/appcompat/appcompat/src/androidTest/res/layout/appcompat_edittext_emoji_activity.xml b/appcompat/appcompat/src/androidTest/res/layout/appcompat_edittext_emoji_activity.xml
index 774a942..7343920 100644
--- a/appcompat/appcompat/src/androidTest/res/layout/appcompat_edittext_emoji_activity.xml
+++ b/appcompat/appcompat/src/androidTest/res/layout/appcompat_edittext_emoji_activity.xml
@@ -56,5 +56,14 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress" />
+
+ <androidx.appcompat.widget.AppCompatEditText
+ android:id="@+id/text_with_digits"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="number"
+ android:digits="01234"
+ />
+
</LinearLayout>
</ScrollView>
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEmojiEditTextHelper.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEmojiEditTextHelper.java
index e3f8b80..1c6b5d3 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEmojiEditTextHelper.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEmojiEditTextHelper.java
@@ -89,7 +89,7 @@
int inputType = mView.getInputType();
mView.setKeyListener(mView.getKeyListener());
// reset the input type and focusable attributes after calling setKeyListener
- mView.setInputType(inputType);
+ mView.setRawInputType(inputType);
mView.setFocusable(wasFocusable);
}
diff --git a/benchmark/common/api/current.txt b/benchmark/common/api/current.txt
index 167ebf4..1874053 100644
--- a/benchmark/common/api/current.txt
+++ b/benchmark/common/api/current.txt
@@ -1,6 +1,18 @@
// Signature format: 4.0
package androidx.benchmark {
+ @RequiresApi(21) public final class Api21Kt {
+ }
+
+ @RequiresApi(24) public final class Api24Kt {
+ }
+
+ @RequiresApi(27) public final class Api27Kt {
+ }
+
+ @RequiresApi(29) public final class Api29Kt {
+ }
+
public final class ArgumentsKt {
}
diff --git a/benchmark/common/api/public_plus_experimental_current.txt b/benchmark/common/api/public_plus_experimental_current.txt
index 2ff6806..39d1008 100644
--- a/benchmark/common/api/public_plus_experimental_current.txt
+++ b/benchmark/common/api/public_plus_experimental_current.txt
@@ -1,6 +1,18 @@
// Signature format: 4.0
package androidx.benchmark {
+ @RequiresApi(21) public final class Api21Kt {
+ }
+
+ @RequiresApi(24) public final class Api24Kt {
+ }
+
+ @RequiresApi(27) public final class Api27Kt {
+ }
+
+ @RequiresApi(29) public final class Api29Kt {
+ }
+
public final class ArgumentsKt {
}
diff --git a/benchmark/common/api/restricted_current.txt b/benchmark/common/api/restricted_current.txt
index b59d142..39bcc5a 100644
--- a/benchmark/common/api/restricted_current.txt
+++ b/benchmark/common/api/restricted_current.txt
@@ -1,6 +1,18 @@
// Signature format: 4.0
package androidx.benchmark {
+ @RequiresApi(21) public final class Api21Kt {
+ }
+
+ @RequiresApi(24) public final class Api24Kt {
+ }
+
+ @RequiresApi(27) public final class Api27Kt {
+ }
+
+ @RequiresApi(29) public final class Api29Kt {
+ }
+
public final class ArgumentsKt {
}
diff --git a/benchmark/common/lint-baseline.xml b/benchmark/common/lint-baseline.xml
deleted file mode 100644
index 1aaaf43..0000000
--- a/benchmark/common/lint-baseline.xml
+++ /dev/null
@@ -1,147 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 7.1.0-alpha02" type="baseline" client="cli" name="Lint" variant="all" version="7.1.0-alpha02">
-
- <issue
- id="BanSynchronizedMethods"
- message="Use of synchronized methods is not recommended"
- errorLine1=" /**"
- errorLine2=" ^">
- <location
- file="src/main/java/androidx/benchmark/simpleperf/ProfileSession.java"
- line="128"
- column="5"/>
- </issue>
-
- <issue
- id="BanSynchronizedMethods"
- message="Use of synchronized methods is not recommended"
- errorLine1=" /**"
- errorLine2=" ^">
- <location
- file="src/main/java/androidx/benchmark/simpleperf/ProfileSession.java"
- line="148"
- column="5"/>
- </issue>
-
- <issue
- id="BanSynchronizedMethods"
- message="Use of synchronized methods is not recommended"
- errorLine1=" /**"
- errorLine2=" ^">
- <location
- file="src/main/java/androidx/benchmark/simpleperf/ProfileSession.java"
- line="163"
- column="5"/>
- </issue>
-
- <issue
- id="BanSynchronizedMethods"
- message="Use of synchronized methods is not recommended"
- errorLine1=" /**"
- errorLine2=" ^">
- <location
- file="src/main/java/androidx/benchmark/simpleperf/ProfileSession.java"
- line="174"
- column="5"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 29; however, the containing class androidx.benchmark.Errors is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" Build.VERSION.SDK_INT >= 29 && !context.applicationInfo.isProfileableByShell"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/benchmark/Errors.kt"
- line="199"
- column="73"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 24; however, the containing class androidx.benchmark.IsolationActivity.Companion is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" powerManager.isSustainedPerformanceModeSupported"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/benchmark/IsolationActivity.kt"
- line="165"
- column="30"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 24; however, the containing class null is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" activity.window.setSustainedPerformanceMode(true)"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/benchmark/IsolationActivity.kt"
- line="174"
- column="37"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 26; however, the containing class null is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" keyguardManager.requestDismissKeyguard(activity, null)"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/benchmark/IsolationActivity.kt"
- line="181"
- column="37"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 27; however, the containing class null is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" activity.setShowWhenLocked(true)"
- errorLine2=" ~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/benchmark/IsolationActivity.kt"
- line="182"
- column="30"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 27; however, the containing class null is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" activity.setTurnScreenOn(true)"
- errorLine2=" ~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/benchmark/IsolationActivity.kt"
- line="183"
- column="30"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 21; however, the containing class androidx.benchmark.Outputs is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" InstrumentationRegistry.getInstrumentation().context.externalMediaDirs"
- errorLine2=" ~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/benchmark/Outputs.kt"
- line="57"
- column="70"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 21; however, the containing class androidx.benchmark.Outputs is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" Environment.getExternalStorageState(it) == Environment.MEDIA_MOUNTED"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/benchmark/Outputs.kt"
- line="59"
- column="37"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 21; however, the containing class androidx.benchmark.ProfilerKt is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" Debug.startMethodTracingSampling(path, bufferSize, Arguments.profilerSampleFrequency)"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/benchmark/Profiler.kt"
- line="98"
- column="15"/>
- </issue>
-
-</issues>
diff --git a/benchmark/common/src/main/java/androidx/benchmark/Api21.kt b/benchmark/common/src/main/java/androidx/benchmark/Api21.kt
new file mode 100644
index 0000000..adb0d14
--- /dev/null
+++ b/benchmark/common/src/main/java/androidx/benchmark/Api21.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+@file:RequiresApi(21)
+
+package androidx.benchmark
+
+import android.content.Context
+import android.os.Debug
+import android.os.Environment
+import androidx.annotation.RequiresApi
+import java.io.File
+
+internal fun startMethodTracingSampling(tracePath: String, bufferSize: Int, intervalUs: Int) {
+ Debug.startMethodTracingSampling(tracePath, bufferSize, intervalUs)
+}
+
+internal fun Context.getFirstMountedMediaDir(): File? {
+ @Suppress("DEPRECATION")
+ return externalMediaDirs.firstOrNull {
+ Environment.getExternalStorageState(it) == Environment.MEDIA_MOUNTED
+ }
+}
diff --git a/benchmark/common/src/main/java/androidx/benchmark/Api24.kt b/benchmark/common/src/main/java/androidx/benchmark/Api24.kt
new file mode 100644
index 0000000..78184d0
--- /dev/null
+++ b/benchmark/common/src/main/java/androidx/benchmark/Api24.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+@file:RequiresApi(24)
+
+package androidx.benchmark
+
+import android.app.Activity
+import android.app.Instrumentation
+import android.content.Context
+import android.os.PowerManager
+import androidx.annotation.RequiresApi
+
+internal fun Instrumentation.isSustainedPerformanceModeSupported(): Boolean {
+ val powerManager = targetContext.getSystemService(Context.POWER_SERVICE) as PowerManager
+ return powerManager.isSustainedPerformanceModeSupported
+}
+
+internal fun Activity.setSustainedPerformanceMode(enable: Boolean) {
+ window.setSustainedPerformanceMode(enable)
+}
\ No newline at end of file
diff --git a/benchmark/common/src/main/java/androidx/benchmark/Api27.kt b/benchmark/common/src/main/java/androidx/benchmark/Api27.kt
new file mode 100644
index 0000000..14328e2
--- /dev/null
+++ b/benchmark/common/src/main/java/androidx/benchmark/Api27.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+@file:RequiresApi(27)
+
+package androidx.benchmark
+
+import android.app.Activity
+import android.app.KeyguardManager
+import androidx.annotation.RequiresApi
+
+internal fun Activity.requestDismissKeyguard() {
+ // technically this could be API 26, but only used on 27
+ val keyguardManager = getSystemService(Activity.KEYGUARD_SERVICE) as KeyguardManager
+ keyguardManager.requestDismissKeyguard(this, null)
+}
+
+internal fun Activity.setShowWhenLocked() {
+ setShowWhenLocked(true)
+}
+
+internal fun Activity.setTurnScreenOn() {
+ setShowWhenLocked(true)
+}
diff --git a/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt b/benchmark/common/src/main/java/androidx/benchmark/Api29.kt
similarity index 67%
copy from benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt
copy to benchmark/common/src/main/java/androidx/benchmark/Api29.kt
index 88234c7..315a4be 100644
--- a/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/Api29.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,13 @@
* limitations under the License.
*/
-package androidx.benchmark.macro
+@file:RequiresApi(29)
-import android.platform.test.rule.DropCachesRule
+package androidx.benchmark
-class BenchmarkClass {
- val rule = DropCachesRule()
+import android.content.Context
+import androidx.annotation.RequiresApi
+
+internal fun Context.isProfileableByShell(): Boolean {
+ return applicationInfo.isProfileableByShell
}
diff --git a/benchmark/common/src/main/java/androidx/benchmark/BenchmarkState.kt b/benchmark/common/src/main/java/androidx/benchmark/BenchmarkState.kt
index 16bd4b5..b7d6f56 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/BenchmarkState.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/BenchmarkState.kt
@@ -431,6 +431,12 @@
ThreadPriority.resetBumpedThread()
}
warmupManager.logInfo()
+
+ if (throttleRemainingRetries == 0) {
+ // If we ran out of throttle detection retries, it's possible the throttle baseline
+ // is incorrect. Force next benchmark to recompute it.
+ ThrottleDetector.resetThrottleBaseline()
+ }
}
private fun computeMaxIterations(): Int {
@@ -557,7 +563,7 @@
internal const val MAX_TEST_ITERATIONS = 1_000_000
internal const val MIN_TEST_ITERATIONS = 1
- private const val THROTTLE_MAX_RETRIES = 3
+ private const val THROTTLE_MAX_RETRIES = 2
private const val THROTTLE_BACKOFF_S = 90L
private var firstBenchmark = true
diff --git a/benchmark/common/src/main/java/androidx/benchmark/Errors.kt b/benchmark/common/src/main/java/androidx/benchmark/Errors.kt
index 04ac4ff..cfd396c 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/Errors.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/Errors.kt
@@ -196,7 +196,7 @@
| be avoided, due to measurement inaccuracy.
""".trimMarginWrapNewlines()
} else if (
- Build.VERSION.SDK_INT >= 29 && !context.applicationInfo.isProfileableByShell
+ Build.VERSION.SDK_INT >= 29 && !context.isProfileableByShell()
) {
warningPrefix += "SIMPLEPERF_"
warningString += """
diff --git a/benchmark/common/src/main/java/androidx/benchmark/IsolationActivity.kt b/benchmark/common/src/main/java/androidx/benchmark/IsolationActivity.kt
index 85564ea..475d34f 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/IsolationActivity.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/IsolationActivity.kt
@@ -16,15 +16,11 @@
package androidx.benchmark
-import android.annotation.SuppressLint
import android.app.Activity
import android.app.Application
-import android.app.KeyguardManager
-import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
-import android.os.PowerManager
import android.os.Process
import android.util.Log
import android.view.WindowManager
@@ -159,28 +155,25 @@
}
internal fun isSustainedPerformanceModeSupported(): Boolean =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- val context = InstrumentationRegistry.getInstrumentation().targetContext
- val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
- powerManager.isSustainedPerformanceModeSupported
+ if (Build.VERSION.SDK_INT >= 24) {
+ InstrumentationRegistry
+ .getInstrumentation()
+ .isSustainedPerformanceModeSupported()
} else {
false
}
private val activityLifecycleCallbacks = object : Application.ActivityLifecycleCallbacks {
- @SuppressLint("NewApi") // window API guarded by [isSustainedPerformanceModeSupported]
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
- if (sustainedPerformanceModeInUse) {
- activity.window.setSustainedPerformanceMode(true)
+ if (sustainedPerformanceModeInUse && Build.VERSION.SDK_INT >= 24) {
+ activity.setSustainedPerformanceMode(true)
}
// Forcibly wake the device, and keep the screen on to prevent benchmark failures.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
- val keyguardManager =
- activity.getSystemService(KEYGUARD_SERVICE) as KeyguardManager
- keyguardManager.requestDismissKeyguard(activity, null)
- activity.setShowWhenLocked(true)
- activity.setTurnScreenOn(true)
+ activity.requestDismissKeyguard()
+ activity.setShowWhenLocked()
+ activity.setTurnScreenOn()
activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
@Suppress("DEPRECATION")
diff --git a/benchmark/common/src/main/java/androidx/benchmark/Outputs.kt b/benchmark/common/src/main/java/androidx/benchmark/Outputs.kt
index 5ebd404..b29ea5b 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/Outputs.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/Outputs.kt
@@ -17,7 +17,6 @@
package androidx.benchmark
import android.os.Build
-import android.os.Environment
import android.util.Log
import androidx.annotation.RestrictTo
import androidx.test.platform.app.InstrumentationRegistry
@@ -54,10 +53,7 @@
Build.VERSION.SDK_INT == 30 -> {
// On Android R, we are using the media directory because that is the directory
// that the shell has access to. Context: b/181601156
- InstrumentationRegistry.getInstrumentation().context.externalMediaDirs
- .firstOrNull {
- Environment.getExternalStorageState(it) == Environment.MEDIA_MOUNTED
- }
+ InstrumentationRegistry.getInstrumentation().context.getFirstMountedMediaDir()
}
Build.VERSION.SDK_INT <= 22 -> {
// prior to API 23, shell didn't have access to externalCacheDir
@@ -65,7 +61,7 @@
}
else -> InstrumentationRegistry.getInstrumentation().context.externalCacheDir
} ?: throw IllegalStateException(
- "Unable to read externalCacheDir for writing files, " +
+ "Unable to select a directory for writing files, " +
"additionalTestOutputDir argument required to declare output dir."
)
diff --git a/benchmark/common/src/main/java/androidx/benchmark/Profiler.kt b/benchmark/common/src/main/java/androidx/benchmark/Profiler.kt
index 2fe9622..4b31557 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/Profiler.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/Profiler.kt
@@ -95,7 +95,7 @@
if (sampled &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
) {
- Debug.startMethodTracingSampling(path, bufferSize, Arguments.profilerSampleFrequency)
+ startMethodTracingSampling(path, bufferSize, Arguments.profilerSampleFrequency)
} else {
Debug.startMethodTracing(path, bufferSize, 0)
}
diff --git a/benchmark/common/src/main/java/androidx/benchmark/ThrottleDetector.kt b/benchmark/common/src/main/java/androidx/benchmark/ThrottleDetector.kt
index 4d8ed0e..abe7a2f 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/ThrottleDetector.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/ThrottleDetector.kt
@@ -60,6 +60,14 @@
}
/**
+ * Called to reset throttle baseline, if throttle detection is firing too regularly, and
+ * inaccurate initial measurement is suspected.
+ */
+ fun resetThrottleBaseline() {
+ initNs = 0L
+ }
+
+ /**
* Makes a guess as to whether the device is currently thermal throttled based on performance
* of single-threaded CPU work.
*/
diff --git a/benchmark/common/src/main/java/androidx/benchmark/simpleperf/ProfileSession.java b/benchmark/common/src/main/java/androidx/benchmark/simpleperf/ProfileSession.java
index 258d710..e38dc41 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/simpleperf/ProfileSession.java
+++ b/benchmark/common/src/main/java/androidx/benchmark/simpleperf/ProfileSession.java
@@ -16,6 +16,7 @@
package androidx.benchmark.simpleperf;
+import android.annotation.SuppressLint;
import android.os.Build;
import android.system.OsConstants;
@@ -67,6 +68,7 @@
*/
@RequiresApi(28)
@RestrictTo(RestrictTo.Scope.LIBRARY)
+@SuppressLint("BanSynchronizedMethods")
public class ProfileSession {
private static final String SIMPLEPERF_PATH_IN_IMAGE = "/system/bin/simpleperf";
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/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/api/current.txt b/benchmark/macro/api/current.txt
index d4c0d57..99ac344 100644
--- a/benchmark/macro/api/current.txt
+++ b/benchmark/macro/api/current.txt
@@ -1,6 +1,9 @@
// Signature format: 4.0
package androidx.benchmark.macro {
+ @RequiresApi(29) public final class Api29Kt {
+ }
+
public abstract sealed class CompilationMode {
}
diff --git a/benchmark/macro/api/public_plus_experimental_current.txt b/benchmark/macro/api/public_plus_experimental_current.txt
index d4c0d57..99ac344 100644
--- a/benchmark/macro/api/public_plus_experimental_current.txt
+++ b/benchmark/macro/api/public_plus_experimental_current.txt
@@ -1,6 +1,9 @@
// Signature format: 4.0
package androidx.benchmark.macro {
+ @RequiresApi(29) public final class Api29Kt {
+ }
+
public abstract sealed class CompilationMode {
}
diff --git a/benchmark/macro/api/restricted_current.txt b/benchmark/macro/api/restricted_current.txt
index a002df4..263de9a 100644
--- a/benchmark/macro/api/restricted_current.txt
+++ b/benchmark/macro/api/restricted_current.txt
@@ -1,6 +1,9 @@
// Signature format: 4.0
package androidx.benchmark.macro {
+ @RequiresApi(29) public final class Api29Kt {
+ }
+
public abstract sealed class CompilationMode {
}
diff --git a/benchmark/macro/lint-baseline.xml b/benchmark/macro/lint-baseline.xml
deleted file mode 100644
index b296a9f..0000000
--- a/benchmark/macro/lint-baseline.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 7.1.0-alpha02" type="baseline" client="cli" name="Lint" variant="all" version="7.1.0-alpha02">
-
- <issue
- id="NewApi"
- message="Call requires API level 29 (current min is 18): `StartupTimingMetric`"
- errorLine1=" val metric = StartupTimingMetric()"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt"
- line="90"
- column="18"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 29 (current min is 18): `configure$lint_module`"
- errorLine1=" metric.configure(packageName)"
- errorLine2=" ~~~~~~~~~">
- <location
- file="src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt"
- line="91"
- column="12"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 29 (current min is 18): `getMetrics$lint_module`"
- errorLine1=" return metric.getMetrics(packageName, tracePath)"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt"
- line="93"
- column="19"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 29; however, the containing class androidx.benchmark.macro.MacrobenchmarkKt is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" !applicationInfo.isProfileableByShell"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/benchmark/macro/Macrobenchmark.kt"
- line="47"
- column="26"/>
- </issue>
-
-</issues>
diff --git a/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
index 2d17c2e..2193dd4 100644
--- a/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
+++ b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
@@ -17,6 +17,7 @@
package androidx.benchmark.macro
import android.content.Intent
+import androidx.annotation.RequiresApi
import androidx.benchmark.macro.perfetto.PerfettoCaptureWrapper
import androidx.benchmark.macro.perfetto.PerfettoHelper.Companion.isAbiSupported
import androidx.benchmark.macro.perfetto.createTempFileFromAsset
@@ -85,6 +86,7 @@
}
}
+@RequiresApi(29)
internal fun measureStartup(packageName: String, measureBlock: () -> Unit): MetricsWithUiState {
val wrapper = PerfettoCaptureWrapper()
val metric = StartupTimingMetric()
diff --git a/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/Api29.kt
similarity index 69%
copy from benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt
copy to benchmark/macro/src/main/java/androidx/benchmark/macro/Api29.kt
index 88234c7..2acdf10 100644
--- a/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/Api29.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,13 @@
* limitations under the License.
*/
+@file:RequiresApi(29)
+
package androidx.benchmark.macro
-import android.platform.test.rule.DropCachesRule
+import android.content.pm.ApplicationInfo
+import androidx.annotation.RequiresApi
-class BenchmarkClass {
- val rule = DropCachesRule()
-}
+internal fun ApplicationInfo.isNotProfileableByShell(): Boolean {
+ return !isProfileableByShell
+}
\ No newline at end of file
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/Macrobenchmark.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index b4a72dd..75540f3 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -44,7 +44,7 @@
}
val errorNotProfileable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- !applicationInfo.isProfileableByShell
+ applicationInfo.isNotProfileableByShell()
} else {
false
}
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/InspectionRelease.kt b/buildSrc/src/main/kotlin/androidx/build/InspectionRelease.kt
index dadf341..dc38caa 100644
--- a/buildSrc/src/main/kotlin/androidx/build/InspectionRelease.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/InspectionRelease.kt
@@ -18,15 +18,34 @@
import androidx.inspection.gradle.InspectionPlugin
import androidx.inspection.gradle.createConsumeInspectionConfiguration
+import androidx.inspection.gradle.createConsumeNonDexedInspectionConfiguration
import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
import org.gradle.api.tasks.Sync
import java.io.File
/**
* Copies artifacts prepared by InspectionPlugin into $destDir/inspection
+ * and $destDir/inspection-nondexed
*/
fun Project.publishInspectionArtifacts() {
- val configuration = createConsumeInspectionConfiguration()
+ publishInspectionConfiguration(
+ "copyInspectionArtifacts",
+ createConsumeInspectionConfiguration(),
+ "inspection"
+ )
+ publishInspectionConfiguration(
+ "copyUndexedInspectionArtifacts",
+ createConsumeNonDexedInspectionConfiguration(),
+ "inspection-nondexed"
+ )
+}
+
+internal fun Project.publishInspectionConfiguration(
+ name: String,
+ configuration: Configuration,
+ dirName: String
+) {
val topLevelProject = this
subprojects { project ->
project.afterEvaluate {
@@ -36,10 +55,10 @@
}
}
- val sync = tasks.register("copyInspectionArtifacts", Sync::class.java) {
+ val sync = tasks.register(name, Sync::class.java) {
it.dependsOn(configuration)
it.from(configuration)
- it.destinationDir = File(getDistributionDirectory(), "inspection")
+ it.destinationDir = File(getDistributionDirectory(), dirName)
}
addToBuildOnServer(sync)
}
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/LintConfiguration.kt b/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
index 8dcd7dc..38d1e22 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -157,6 +157,9 @@
isNoLines = false
isQuiet = true
+ // We run lint on each library, so we don't want transitive checking of each dependency
+ isCheckDependencies = false
+
fatal("VisibleForTests")
// Disable dependency checks that suggest to change them. We want libraries to be
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..b7cf31c 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/**",
@@ -595,11 +595,11 @@
"androidx/emoji2/**",
"androidx/enterprise/**",
"androidx/exifinterface/**",
-// "androidx/fragment/**",
+ "androidx/fragment/**",
"androidx/gridlayout/**",
"androidx/health/**",
"androidx/heifwriter/**",
-// "androidx/hilt/**",
+ "androidx/hilt/**",
"androidx/interpolator/**",
// "androidx/leanback/**",
"androidx/legacy/**",
@@ -607,13 +607,13 @@
"androidx/loader/**",
"androidx/localbroadcastmanager/**",
"androidx/media/**",
-// "androidx/media2/**",
+ "androidx/media2/**",
"androidx/mediarouter/**",
"androidx/navigation/**",
"androidx/paging/**",
"androidx/palette/**",
"androidx/percentlayout/**",
-// "androidx/preference/**",
+ "androidx/preference/**",
"androidx/print/**",
"androidx/profileinstaller/**",
"androidx/recommendation/**",
@@ -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/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-extensions/build.gradle b/camera/camera-extensions/build.gradle
index 03abc84..d1157c4 100644
--- a/camera/camera-extensions/build.gradle
+++ b/camera/camera-extensions/build.gradle
@@ -64,12 +64,6 @@
android {
defaultConfig {
minSdkVersion 21
-
- buildConfigField "String", "CAMERA_VERSION", "\"1.1.0\""
- }
- buildFeatures {
- // Enable generating BuildConfig.java since support library default disable it.
- buildConfig = true
}
// Use Robolectric 4.+
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/VersionName.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/VersionName.java
index 47b7945..19a4e1d 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/VersionName.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/VersionName.java
@@ -17,14 +17,14 @@
package androidx.camera.extensions.internal;
import androidx.annotation.NonNull;
-import androidx.camera.extensions.BuildConfig;
/**
* The version of CameraX extension releases.
*/
public class VersionName {
- /* The current version of the CameraX extension. */
- private static final VersionName CURRENT = new VersionName(BuildConfig.CAMERA_VERSION);
+ // Current version of vendor library implementation that the CameraX extension supports. This
+ // needs to be increased along with the version of vendor library interface.
+ private static final VersionName CURRENT = new VersionName("1.1.0");
@NonNull
public static VersionName getCurrentVersion() {
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/PendingRecording.java b/camera/camera-video/src/main/java/androidx/camera/video/PendingRecording.java
index 20b28e6..cb37955 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/PendingRecording.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/PendingRecording.java
@@ -95,8 +95,14 @@
/**
* Enables audio to be recorded for this recording.
*
- * <p>By default, the recording will be created without audio recording. To enable audio
- * recording, the {@link Manifest.permission#RECORD_AUDIO} has to be granted.
+ * <p>This method must be called prior to {@link #start()} to enable audio in the recording. If
+ * this method is not called, the {@link ActiveRecording} generated by {@link #start()}
+ * will not contain audio, and {@link RecordingStats#getAudioState()} will always return
+ * {@link RecordingStats#AUDIO_DISABLED} for all {@link RecordingStats} send to the listener
+ * set by {@link #withEventListener(Executor, Consumer)}.
+ *
+ * <p>This method requires the caller to hold the permission
+ * {@link android.Manifest.permission#RECORD_AUDIO}.
*
* @return this pending recording
*/
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..95c911e 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();
});
@@ -1054,20 +1034,8 @@
mRecordingDurationNs = TimeUnit.MICROSECONDS.toNanos(
encodedData.getPresentationTimeUs() - mFirstRecordingVideoDataTimeUs);
- updateVideoRecordEvent(
- VideoRecordEvent.status(
- mRunningRecording.getOutputOptions(),
- getCurrentRecordingStats()));
+ updateStatusEvent();
}
- mRecordingDurationNs = TimeUnit.MICROSECONDS.toNanos(
- encodedData.getPresentationTimeUs() - mFirstRecordingVideoDataTimeUs);
- mRecordingBytes += encodedData.size();
-
- Preconditions.checkNotNull(mMediaMuxer).writeSampleData(mVideoTrackIndex,
- encodedData.getByteBuffer(), encodedData.getBufferInfo());
- encodedData.close();
-
- updateStatusEvent();
}
@Override
@@ -1253,7 +1221,7 @@
}
@ExecutedBy("mSequentialExecutor")
- private void releaseInternal() {
+ private void resetInternal() {
if (mAudioEncoder != null) {
mAudioEncoder.release();
mAudioSource = null;
@@ -1268,7 +1236,7 @@
}
mSurfaceRequested.set(false);
- setState(State.RELEASED);
+ setState(State.INITIALIZING);
}
private int internalAudioStateToEventAudioState(AudioState audioState) {
@@ -1349,8 +1317,8 @@
setAudioState(AudioState.INITIALIZING);
}
synchronized (mLock) {
- if (getObservableData(mState) == State.RELEASING) {
- releaseInternal();
+ if (getObservableData(mState) == State.RESETING) {
+ resetInternal();
} else {
setState(State.IDLING);
}
@@ -1460,7 +1428,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/RecordingStats.java b/camera/camera-video/src/main/java/androidx/camera/video/RecordingStats.java
index 8c71710..9bc8a82 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/RecordingStats.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/RecordingStats.java
@@ -19,26 +19,40 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
+import androidx.core.util.Consumer;
import androidx.core.util.Preconditions;
import com.google.auto.value.AutoValue;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
/**
- * RecordingStats keeps track of the current recording’s statistics. It is a snapshot of things
- * like recorded duration and recorded file size.
+ * A snapshot of statistics about an {@link ActiveRecording} at a point in time.
+ *
+ * <p>Recording stats provide information about a recording such as file size, duration and other
+ * useful statistics which may be useful for tracking the state of a recording.
+ *
+ * <p>Recording stats are generated for every {@link VideoRecordEvent} and can be retrieved via
+ * {@link VideoRecordEvent#getRecordingStats()}.
+ * @see PendingRecording#withEventListener(Executor, Consumer)
*/
@AutoValue
public abstract class RecordingStats {
/**
* The recording is being recorded with audio data.
+ *
+ * <p>When audio is recording, the resulting video file will contain an audio track.
*/
public static final int AUDIO_RECORDING = 0;
/**
* The recording is disabled.
+ *
+ * <p>This audio state results from a {@link PendingRecording} that was
+ * {@linkplain PendingRecording#start() started} without calling
+ * {@link PendingRecording#withAudioEnabled()}.
*/
public static final int AUDIO_DISABLED = 1;
/**
@@ -71,13 +85,35 @@
return new AutoValue_RecordingStats(duration, bytes, audioState);
}
- /** Returns current recorded duration in nano seconds. */
+ /**
+ * Returns current recorded duration in nanoseconds.
+ *
+ * <p>The duration represents the realtime number of nanoseconds that have transpired since
+ * the recording started, excluding intervals where the recording was paused.
+ * @return the duration, in nanoseconds, of the recording at the time of these recording stats
+ * being generated.
+ */
public abstract long getRecordedDurationNanos();
- /** Returns current recorded bytes. */
+ /**
+ * Returns the number of bytes recorded.
+ *
+ * <p>The number of bytes recorded includes bytes stored for video and for audio, if applicable.
+ * @return the total number of bytes stored for the recording at the time of these recording
+ * stats being generated.
+ */
public abstract long getNumBytesRecorded();
- /** Returns current audio state. */
+ /**
+ * Returns the state of audio in the recording.
+ *
+ * <p>The audio state describes whether audio is enabled for the recording and if audio is
+ * currently recording or is silenced due to system priority or errors.
+ *
+ * @return The state of the audio at the time of these recording stats being generated. One of
+ * {@link #AUDIO_RECORDING}, {@link #AUDIO_DISABLED}, {@link #AUDIO_SOURCE_SILENCED}, or
+ * {@link #AUDIO_ENCODER_ERROR}.
+ */
@AudioState
public abstract int getAudioState();
}
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/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/car/NavigationScreen.java b/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/car/NavigationScreen.java
index 00def13..f9422cf 100644
--- a/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/car/NavigationScreen.java
+++ b/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/car/NavigationScreen.java
@@ -183,6 +183,17 @@
new CarIcon.Builder(
IconCompat.createWithResource(
getCarContext(),
+ R.drawable.ic_recenter_24))
+ .build())
+ .setOnClickListener(
+ () -> mSurfaceRenderer.handleRecenter())
+ .build())
+ .addAction(
+ new Action.Builder()
+ .setIcon(
+ new CarIcon.Builder(
+ IconCompat.createWithResource(
+ getCarContext(),
R.drawable.ic_zoom_out_24))
.build())
.setOnClickListener(
diff --git a/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/car/SurfaceRenderer.java b/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/car/SurfaceRenderer.java
index 80fa55f..38df75b 100644
--- a/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/car/SurfaceRenderer.java
+++ b/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/car/SurfaceRenderer.java
@@ -200,7 +200,7 @@
renderFrame();
}
- /** Handles a map zoom-in and zoom-out events. */
+ /** Handles the map zoom-in and zoom-out events. */
public void handleScale(float focusX, float focusY, float scaleFactor) {
synchronized (this) {
float x = focusX;
@@ -228,6 +228,13 @@
}
}
+ /** Handles the map re-centering events. */
+ public void handleRecenter() {
+ // Resetting the map matrix will trigger the initialization logic in renderFrame().
+ mBackgroundMapMatrix.reset();
+ renderFrame();
+ }
+
/** Updates the markers drawn on the surface. */
public void updateMarkerVisibility(boolean showMarkers, int numMarkers, int activeMarker) {
mShowMarkers = showMarkers;
diff --git a/car/app/app-samples/navigation/common/src/main/res/drawable/ic_recenter_24.xml b/car/app/app-samples/navigation/common/src/main/res/drawable/ic_recenter_24.xml
new file mode 100644
index 0000000..52cfb954
--- /dev/null
+++ b/car/app/app-samples/navigation/common/src/main/res/drawable/ic_recenter_24.xml
@@ -0,0 +1,25 @@
+<!--
+ 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M12,2C6.49,2 2,6.49 2,12s4.49,10 10,10 10,-4.49 10,-10S17.51,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM15,12c0,1.66 -1.34,3 -3,3s-3,-1.34 -3,-3 1.34,-3 3,-3 3,1.34 3,3z"/>
+</vector>
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-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/misc/CarHardwareDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/misc/CarHardwareDemoScreen.java
index 0e251b4..b1e1e09 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/misc/CarHardwareDemoScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/misc/CarHardwareDemoScreen.java
@@ -21,10 +21,13 @@
import androidx.car.app.Screen;
import androidx.car.app.model.Action;
import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarIcon;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.NavigationTemplate;
+import androidx.car.app.sample.showcase.common.R;
import androidx.car.app.sample.showcase.common.ShowcaseSession;
import androidx.car.app.sample.showcase.common.renderer.CarHardwareRenderer;
+import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
@@ -41,7 +44,8 @@
Lifecycle lifecycle = getLifecycle();
lifecycle.addObserver(new DefaultLifecycleObserver() {
- @NonNull final ShowcaseSession mShowcaseSession = showcaseSession;
+ @NonNull
+ final ShowcaseSession mShowcaseSession = showcaseSession;
@Override
public void onResume(@NonNull LifecycleOwner owner) {
@@ -64,6 +68,17 @@
public Template onGetTemplate() {
ActionStrip actionStrip =
new ActionStrip.Builder()
+ // Add a Button to show the CarHardware info screen
+ .addAction(new Action.Builder()
+ .setIcon(
+ new CarIcon.Builder(
+ IconCompat.createWithResource(
+ getCarContext(),
+ R.drawable.info_gm_grey_24dp))
+ .build())
+ .setOnClickListener(() -> getScreenManager().push(
+ new CarHardwareInfoScreen(getCarContext())))
+ .build())
.addAction(
new Action.Builder()
.setTitle("BACK")
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/misc/CarHardwareInfoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/misc/CarHardwareInfoScreen.java
new file mode 100644
index 0000000..8adb771
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/misc/CarHardwareInfoScreen.java
@@ -0,0 +1,265 @@
+/*
+ * 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.car.app.sample.showcase.common.misc;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.car.app.CarContext;
+import androidx.car.app.Screen;
+import androidx.car.app.hardware.CarHardwareManager;
+import androidx.car.app.hardware.common.CarValue;
+import androidx.car.app.hardware.common.OnCarDataAvailableListener;
+import androidx.car.app.hardware.info.CarInfo;
+import androidx.car.app.hardware.info.EnergyProfile;
+import androidx.car.app.hardware.info.Model;
+import androidx.car.app.model.Action;
+import androidx.car.app.model.Pane;
+import androidx.car.app.model.PaneTemplate;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Template;
+import androidx.core.content.ContextCompat;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Creates a screen that show the static information (such as model and energy profile) available
+ * via CarHardware interfaces.
+ */
+public final class CarHardwareInfoScreen extends Screen {
+ private static final String TAG = "showcase";
+
+ boolean mHasModelPermission;
+ boolean mHasEnergyProfilePermission;
+ final Executor mCarHardwareExecutor;
+
+ /**
+ * Value fetched from CarHardwareManager containing model information.
+ *
+ * <p>It is requested asynchronously and can be {@code null} until the response is
+ * received.
+ */
+ @Nullable
+ Model mModel;
+
+ /**
+ * Value fetched from CarHardwareManager containing what type of fuel/ports the car has.
+ *
+ * <p>It is requested asynchronously and can be {@code null} until the response is
+ * received.
+ */
+ @Nullable
+ EnergyProfile mEnergyProfile;
+
+ OnCarDataAvailableListener<Model> mModelListener = data -> {
+ synchronized (this) {
+ Log.i(TAG, "Received model information: " + data);
+ mModel = data;
+ invalidate();
+ }
+ };
+
+ OnCarDataAvailableListener<EnergyProfile> mEnergyProfileListener = data -> {
+ synchronized (this) {
+ Log.i(TAG, "Received energy profile information: " + data);
+ mEnergyProfile = data;
+ invalidate();
+ }
+ };
+
+ public CarHardwareInfoScreen(@NonNull CarContext carContext) {
+ super(carContext);
+ mCarHardwareExecutor = ContextCompat.getMainExecutor(getCarContext());
+ Lifecycle lifecycle = getLifecycle();
+ lifecycle.addObserver(new DefaultLifecycleObserver() {
+
+ @Override
+ public void onCreate(@NonNull LifecycleOwner owner) {
+ CarHardwareManager carHardwareManager =
+ getCarContext().getCarService(CarHardwareManager.class);
+ CarInfo carInfo = carHardwareManager.getCarInfo();
+
+ // Request any single shot values.
+ mModel = null;
+ try {
+ carInfo.fetchModel(mCarHardwareExecutor, mModelListener);
+ mHasModelPermission = true;
+ } catch (SecurityException e) {
+ mHasModelPermission = false;
+ }
+
+ mEnergyProfile = null;
+ try {
+ carInfo.fetchModel(mCarHardwareExecutor, mModelListener);
+ mHasEnergyProfilePermission = true;
+ } catch (SecurityException e) {
+ mHasEnergyProfilePermission = false;
+ }
+ carInfo.fetchEnergyProfile(mCarHardwareExecutor, mEnergyProfileListener);
+ }
+
+ });
+ }
+
+ @NonNull
+ @Override
+ public Template onGetTemplate() {
+ Pane.Builder paneBuilder = new Pane.Builder();
+ if (allInfoAvailable()) {
+ Row.Builder modelRowBuilder = new Row.Builder()
+ .setTitle("Model Information");
+ if (!mHasModelPermission) {
+ modelRowBuilder.addText("No Model Permission.");
+ } else {
+ StringBuilder info = new StringBuilder();
+ if (mModel.getManufacturer().getStatus() != CarValue.STATUS_SUCCESS) {
+ info.append("Manufacturer unavailable, ");
+ } else {
+ info.append(mModel.getManufacturer().getValue());
+ info.append(", ");
+ }
+ if (mModel.getName().getStatus() != CarValue.STATUS_SUCCESS) {
+ info.append("Model unavailable, ");
+ } else {
+ info.append(mModel.getName().getValue());
+ info.append(", ");
+ }
+ if (mModel.getYear().getStatus() != CarValue.STATUS_SUCCESS) {
+ info.append("Year unavailable");
+ } else {
+ info.append(mModel.getYear().getValue());
+ }
+ modelRowBuilder.addText(info);
+ }
+ paneBuilder.addRow(modelRowBuilder.build());
+
+ Row.Builder energyProfileRowBuilder = new Row.Builder()
+ .setTitle("Energy Profile");
+ if (!mHasModelPermission) {
+ energyProfileRowBuilder.addText("No Energy Profile Permission.");
+ } else {
+ StringBuilder fuelInfo = new StringBuilder();
+ if (mEnergyProfile.getFuelTypes().getStatus() != CarValue.STATUS_SUCCESS) {
+ fuelInfo.append("Fuel Types: Unavailable.");
+ } else {
+ fuelInfo.append("Fuel Types: ");
+ for (int fuelType : mEnergyProfile.getFuelTypes().getValue()) {
+ fuelInfo.append(fuelTypeAsString(fuelType));
+ fuelInfo.append(" ");
+ }
+ }
+ energyProfileRowBuilder.addText(fuelInfo);
+ StringBuilder evInfo = new StringBuilder();
+ if (mEnergyProfile.getEvConnectorTypes().getStatus() != CarValue.STATUS_SUCCESS) {
+ evInfo.append(" EV Connector Types: Unavailable.");
+ } else {
+ evInfo.append("EV Connector Types: ");
+ for (int connectorType : mEnergyProfile.getEvConnectorTypes().getValue()) {
+ evInfo.append(evConnectorAsString(connectorType));
+ evInfo.append(" ");
+ }
+ }
+ energyProfileRowBuilder.addText(evInfo);
+ }
+ paneBuilder.addRow(energyProfileRowBuilder.build());
+ } else {
+ paneBuilder.setLoading(true);
+ }
+ return new PaneTemplate.Builder(paneBuilder.build())
+ .setHeaderAction(Action.BACK)
+ .setTitle("Car Hardware Information")
+ .build();
+ }
+
+ private boolean allInfoAvailable() {
+ if (mHasModelPermission && mModel == null) {
+ return false;
+ }
+ if (mHasEnergyProfilePermission && mEnergyProfile == null) {
+ return false;
+ }
+ return true;
+ }
+
+ private String fuelTypeAsString(int fuelType) {
+ switch (fuelType) {
+ case EnergyProfile.FUEL_TYPE_UNLEADED:
+ return "UNLEADED";
+ case EnergyProfile.FUEL_TYPE_LEADED:
+ return "LEADED";
+ case EnergyProfile.FUEL_TYPE_DIESEL_1:
+ return "DIESEL_1";
+ case EnergyProfile.FUEL_TYPE_DIESEL_2:
+ return "DIESEL_2";
+ case EnergyProfile.FUEL_TYPE_BIODIESEL:
+ return "BIODIESEL";
+ case EnergyProfile.FUEL_TYPE_E85:
+ return "E85";
+ case EnergyProfile.FUEL_TYPE_LPG:
+ return "LPG";
+ case EnergyProfile.FUEL_TYPE_CNG:
+ return "CNG";
+ case EnergyProfile.FUEL_TYPE_LNG:
+ return "LNG";
+ case EnergyProfile.FUEL_TYPE_ELECTRIC:
+ return "ELECTRIC";
+ case EnergyProfile.FUEL_TYPE_HYDROGEN:
+ return "HYDROGEN";
+ case EnergyProfile.FUEL_TYPE_OTHER:
+ return "OTHER";
+ case EnergyProfile.FUEL_TYPE_UNKNOWN:
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ private String evConnectorAsString(int evConnectorType) {
+ switch (evConnectorType) {
+ case EnergyProfile.EVCONNECTOR_TYPE_J1772:
+ return "J1772";
+ case EnergyProfile.EVCONNECTOR_TYPE_MENNEKES:
+ return "MENNEKES";
+ case EnergyProfile.EVCONNECTOR_TYPE_CHADEMO:
+ return "CHADEMO";
+ case EnergyProfile.EVCONNECTOR_TYPE_COMBO_1:
+ return "COMBO_1";
+ case EnergyProfile.EVCONNECTOR_TYPE_COMBO_2:
+ return "COMBO_2";
+ case EnergyProfile.EVCONNECTOR_TYPE_TESLA_ROADSTER:
+ return "TESLA_ROADSTER";
+ case EnergyProfile.EVCONNECTOR_TYPE_TESLA_HPWC:
+ return "TESLA_HPWC";
+ case EnergyProfile.EVCONNECTOR_TYPE_TESLA_SUPERCHARGER:
+ return "TESLA_SUPERCHARGER";
+ case EnergyProfile.EVCONNECTOR_TYPE_GBT:
+ return "GBT";
+ case EnergyProfile.EVCONNECTOR_TYPE_GBT_DC:
+ return "GBT_DC";
+ case EnergyProfile.EVCONNECTOR_TYPE_SCAME:
+ return "SCAME";
+ case EnergyProfile.EVCONNECTOR_TYPE_OTHER:
+ return "OTHER";
+ case EnergyProfile.EVCONNECTOR_TYPE_UNKNOWN:
+ default:
+ return "UNKNOWN";
+ }
+ }
+}
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/misc/RequestPermissionScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/misc/RequestPermissionScreen.java
index ad5e150..4a8d9b1 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/misc/RequestPermissionScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/misc/RequestPermissionScreen.java
@@ -124,11 +124,13 @@
OnClickListener listener = ParkedOnlyOnClickListener.create(() -> {
getCarContext().requestPermissions(
permissions,
- (approved, rejected) -> CarToast.makeText(
- getCarContext(),
- String.format("Approved: %s Rejected: %s", approved, rejected),
- CarToast.LENGTH_LONG).show());
- finish();
+ (approved, rejected) -> {
+ CarToast.makeText(
+ getCarContext(),
+ String.format("Approved: %s Rejected: %s", approved, rejected),
+ CarToast.LENGTH_LONG).show();
+ finish();
+ });
});
Action action = new Action.Builder()
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/renderer/CarHardwareRenderer.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/renderer/CarHardwareRenderer.java
index 5043a86..49353cd 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/renderer/CarHardwareRenderer.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/renderer/CarHardwareRenderer.java
@@ -34,10 +34,8 @@
import androidx.car.app.hardware.info.CarSensors;
import androidx.car.app.hardware.info.Compass;
import androidx.car.app.hardware.info.EnergyLevel;
-import androidx.car.app.hardware.info.EnergyProfile;
import androidx.car.app.hardware.info.Gyroscope;
import androidx.car.app.hardware.info.Mileage;
-import androidx.car.app.hardware.info.Model;
import androidx.car.app.hardware.info.Speed;
import androidx.car.app.hardware.info.TollCard;
import androidx.core.content.ContextCompat;
@@ -49,17 +47,14 @@
public final class CarHardwareRenderer implements Renderer {
private static final String TAG = "showcase";
- private static final int VERTICAL_TEXT_MARGIN_FROM_TOP = 20;
+ private static final int ROW_SPACING = 10;
+ private static final int LEFT_MARGIN = 15;
private final Executor mCarHardwareExecutor;
private final Paint mCarInfoPaint = new Paint();
private final CarContext mCarContext;
@Nullable
- Model mModel;
- @Nullable
- EnergyProfile mEnergyProfile;
- @Nullable
TollCard mTollCard;
@Nullable
EnergyLevel mEnergyLevel;
@@ -77,8 +72,6 @@
CarHardwareLocation mCarHardwareLocation;
@Nullable
private Runnable mRequestRenderRunnable;
- private boolean mHasModelPermission;
- private boolean mHasEnergyProfilePermission;
private boolean mHasTollCardPermission;
private boolean mHasEnergyLevelPermission;
private boolean mHasSpeedPermission;
@@ -88,20 +81,6 @@
private boolean mHasCompassPermission;
private boolean mHasCarHardwareLocationPermission;
- private OnCarDataAvailableListener<Model> mModelListener = data -> {
- synchronized (this) {
- Log.i(TAG, "Received model information: " + data);
- mModel = data;
- requestRenderFrame();
- }
- };
- private OnCarDataAvailableListener<EnergyProfile> mEnergyProfileListener = data -> {
- synchronized (this) {
- Log.i(TAG, "Received energy profile information: " + data);
- mEnergyProfile = data;
- requestRenderFrame();
- }
- };
private OnCarDataAvailableListener<TollCard> mTollListener = data -> {
synchronized (this) {
Log.i(TAG, "Received toll information:" + data);
@@ -164,7 +143,6 @@
mCarInfoPaint.setColor(Color.BLACK);
mCarInfoPaint.setAntiAlias(true);
mCarInfoPaint.setStyle(Paint.Style.STROKE);
- mCarInfoPaint.setTextAlign(Paint.Align.CENTER);
mCarHardwareExecutor = ContextCompat.getMainExecutor(mCarContext);
}
@@ -176,33 +154,14 @@
CarInfo carInfo = carHardwareManager.getCarInfo();
CarSensors carSensors = carHardwareManager.getCarSensors();
- // Request any single shot values.
- mModel = null;
- try {
- carInfo.fetchModel(mCarHardwareExecutor, mModelListener);
- mHasModelPermission = true;
- } catch (SecurityException e) {
- mHasModelPermission = false;
- }
-
- mEnergyProfile = null;
- try {
- carInfo.fetchModel(mCarHardwareExecutor, mModelListener);
- mHasEnergyProfilePermission = true;
- } catch (SecurityException e) {
- mHasEnergyProfilePermission = false;
- }
- carInfo.fetchEnergyProfile(mCarHardwareExecutor, mEnergyProfileListener);
-
// Request car info subscription items.
mTollCard = null;
try {
- carInfo.fetchModel(mCarHardwareExecutor, mModelListener);
+ carInfo.addTollListener(mCarHardwareExecutor, mTollListener);
mHasTollCardPermission = true;
} catch (SecurityException e) {
mHasTollCardPermission = false;
}
- carInfo.addTollListener(mCarHardwareExecutor, mTollListener);
mEnergyLevel = null;
try {
@@ -346,90 +305,36 @@
@Override
public void renderFrame(@NonNull Canvas canvas, @Nullable Rect visibleArea,
@Nullable Rect stableArea) {
- if (visibleArea != null) {
- if (visibleArea.isEmpty()) {
+ if (stableArea != null) {
+ if (stableArea.isEmpty()) {
// No inset set. The entire area is considered safe to draw.
- visibleArea.set(0, 0, canvas.getWidth() - 1, canvas.getHeight() - 1);
+ stableArea.set(0, 0, canvas.getWidth() - 1, canvas.getHeight() - 1);
}
+ int height = stableArea.height() / 8;
+ int updatedSize = height - ROW_SPACING;
+ mCarInfoPaint.setTextSize(updatedSize);
+
+ canvas.drawRect(stableArea, mCarInfoPaint);
+
Paint.FontMetrics fm = mCarInfoPaint.getFontMetrics();
- float height = fm.descent - fm.ascent;
- float verticalPos = visibleArea.top + VERTICAL_TEXT_MARGIN_FROM_TOP;
-
- // Prepare text for Make, Model, Year
- StringBuilder info = new StringBuilder();
- if (!mHasModelPermission) {
- info.append("No Model Permission.");
- } else if (mModel == null) {
- info.append("Fetching model info.");
- } else {
- if (mModel.getManufacturer().getStatus() != CarValue.STATUS_SUCCESS) {
- info.append("Manufacturer unavailable, ");
- } else {
- info.append(mModel.getManufacturer().getValue());
- info.append(",");
- }
- if (mModel.getName().getStatus() != CarValue.STATUS_SUCCESS) {
- info.append("Model unavailable, ");
- } else {
- info.append(mModel.getName());
- info.append(",");
- }
- if (mModel.getYear().getStatus() != CarValue.STATUS_SUCCESS) {
- info.append("Year unavailable.");
- } else {
- info.append(mModel.getYear());
- }
- }
- canvas.drawText(info.toString(), visibleArea.centerX(), verticalPos, mCarInfoPaint);
- verticalPos += height;
-
- // Prepare text for Energy Profile
- info = new StringBuilder();
- if (!mHasEnergyProfilePermission) {
- info.append("No EnergyProfile Permission.");
- } else if (mEnergyProfile == null) {
- info.append("Fetching EnergyProfile.");
- } else {
- if (mEnergyProfile.getFuelTypes().getStatus() != CarValue.STATUS_SUCCESS) {
- info.append("Fuel Types: Unavailable. ");
- } else {
- info.append("Fuel Types: [");
- for (int fuelType : mEnergyProfile.getFuelTypes().getValue()) {
- info.append(fuelType);
- info.append(" ");
- }
- info.append("].");
- }
- if (mEnergyProfile.getEvConnectorTypes().getStatus() != CarValue.STATUS_SUCCESS) {
- info.append(" EV Connector Types: Unavailable. ");
- } else {
- info.append("EV Connector Types:[");
- for (int connectorType : mEnergyProfile.getEvConnectorTypes().getValue()) {
- info.append(connectorType);
- info.append(" ");
- }
- info.append("]");
- }
- }
- canvas.drawText(info.toString(), visibleArea.centerX(), verticalPos, mCarInfoPaint);
- verticalPos += height;
+ float verticalPos = stableArea.top - fm.ascent;
// Prepare text for Toll card status
- info = new StringBuilder();
+ StringBuilder info = new StringBuilder();
if (!mHasTollCardPermission) {
info.append("No TollCard Permission.");
} else if (mTollCard == null) {
info.append("Fetching Toll information.");
} else {
if (mTollCard.getCardState().getStatus() != CarValue.STATUS_SUCCESS) {
- info.append("Toll card state: Unavailable. ");
+ info.append("Toll card state: N/A. ");
} else {
info.append("Toll card state: ");
info.append(mTollCard.getCardState().getValue());
}
}
- canvas.drawText(info.toString(), visibleArea.centerX(), verticalPos, mCarInfoPaint);
+ canvas.drawText(info.toString(), LEFT_MARGIN, verticalPos, mCarInfoPaint);
verticalPos += height;
// Prepare text for Energy Level
@@ -440,35 +345,35 @@
info.append("Fetching Energy Level.");
} else {
if (mEnergyLevel.getEnergyIsLow().getStatus() != CarValue.STATUS_SUCCESS) {
- info.append("Low energy: Unavailable. ");
+ info.append("Low energy: N/A. ");
} else {
info.append("Low energy: ");
info.append(mEnergyLevel.getEnergyIsLow().getValue());
info.append(" ");
}
if (mEnergyLevel.getRangeRemainingMeters().getStatus() != CarValue.STATUS_SUCCESS) {
- info.append("Range: Unavailable. ");
+ info.append("Range: N/A. ");
} else {
info.append("Range: ");
info.append(mEnergyLevel.getRangeRemainingMeters().getValue());
info.append(" m. ");
}
if (mEnergyLevel.getFuelPercent().getStatus() != CarValue.STATUS_SUCCESS) {
- info.append("Fuel Percent: Unavailable. ");
+ info.append("Fuel: N/A. ");
} else {
- info.append("Fuel Percent: ");
+ info.append("Fuel: ");
info.append(mEnergyLevel.getFuelPercent().getValue());
info.append("% ");
}
if (mEnergyLevel.getBatteryPercent().getStatus() != CarValue.STATUS_SUCCESS) {
- info.append("Battery Percent: Unavailable. ");
+ info.append("Battery: N/A. ");
} else {
- info.append("Battery Percent: ");
+ info.append("Battery: ");
info.append(mEnergyLevel.getBatteryPercent().getValue());
info.append("% ");
}
}
- canvas.drawText(info.toString(), visibleArea.centerX(), verticalPos, mCarInfoPaint);
+ canvas.drawText(info.toString(), LEFT_MARGIN, verticalPos, mCarInfoPaint);
verticalPos += height;
// Prepare text for Speed
@@ -480,28 +385,28 @@
} else {
if (mSpeed.getDisplaySpeedMetersPerSecond().getStatus()
!= CarValue.STATUS_SUCCESS) {
- info.append("Display Speed: Unavailable. ");
+ info.append("Display Speed: N/A. ");
} else {
info.append("Display Speed: ");
info.append(mSpeed.getDisplaySpeedMetersPerSecond().getValue());
info.append(" m/s. ");
}
if (mSpeed.getRawSpeedMetersPerSecond().getStatus() != CarValue.STATUS_SUCCESS) {
- info.append("Raw Speed: Unavailable. ");
+ info.append("Raw Speed: N/A. ");
} else {
info.append("Raw Speed: ");
info.append(mSpeed.getRawSpeedMetersPerSecond().getValue());
info.append(" m/s. ");
}
if (mSpeed.getSpeedDisplayUnit().getStatus() != CarValue.STATUS_SUCCESS) {
- info.append("Speed Display Unit: Unavailable.");
+ info.append("Unit: N/A.");
} else {
- info.append("Speed Display Unit: ");
+ info.append("Unit: ");
info.append(mSpeed.getSpeedDisplayUnit().getValue());
info.append(" ");
}
}
- canvas.drawText(info.toString(), visibleArea.centerX(), verticalPos, mCarInfoPaint);
+ canvas.drawText(info.toString(), LEFT_MARGIN, verticalPos, mCarInfoPaint);
verticalPos += height;
// Prepare text for Odometer
@@ -512,21 +417,21 @@
info.append("Fetching mileage.");
} else {
if (mMileage.getOdometerMeters().getStatus() != CarValue.STATUS_SUCCESS) {
- info.append("Odometer: Unavailable. ");
+ info.append("Odometer: N/A. ");
} else {
info.append("Odometer: ");
info.append(mMileage.getOdometerMeters().getValue());
info.append(" m. ");
}
if (mMileage.getDistanceDisplayUnit().getStatus() != CarValue.STATUS_SUCCESS) {
- info.append("Mileage Display Unit: Unavailable.");
+ info.append("Unit: N/A.");
} else {
- info.append("Mileage Display Unit: ");
+ info.append("Unit: ");
info.append(mMileage.getDistanceDisplayUnit().getValue());
info.append(" ");
}
}
- canvas.drawText(info.toString(), visibleArea.centerX(), verticalPos, mCarInfoPaint);
+ canvas.drawText(info.toString(), LEFT_MARGIN, verticalPos, mCarInfoPaint);
verticalPos += height;
// Prepare text for Accelerometer
@@ -537,13 +442,13 @@
info.append("Fetching accelerometer");
} else {
if (mAccelerometer.getForces().getStatus() != CarValue.STATUS_SUCCESS) {
- info.append("Accelerometer unavailable.");
+ info.append("Accelerometer N/A.");
} else {
info.append("Accelerometer: ");
appendFloatList(info, mAccelerometer.getForces().getValue());
}
}
- canvas.drawText(info.toString(), visibleArea.centerX(), verticalPos, mCarInfoPaint);
+ canvas.drawText(info.toString(), LEFT_MARGIN, verticalPos, mCarInfoPaint);
verticalPos += height;
// Prepare text for Gyroscope
@@ -554,13 +459,13 @@
info.append("Fetching gyroscope");
} else {
if (mGyroscope.getRotations().getStatus() != CarValue.STATUS_SUCCESS) {
- info.append("Gyroscope unavailable.");
+ info.append("Gyroscope N/A.");
} else {
info.append("Gyroscope: ");
appendFloatList(info, mGyroscope.getRotations().getValue());
}
}
- canvas.drawText(info.toString(), visibleArea.centerX(), verticalPos, mCarInfoPaint);
+ canvas.drawText(info.toString(), LEFT_MARGIN, verticalPos, mCarInfoPaint);
verticalPos += height;
// Prepare text for Compass
@@ -571,13 +476,13 @@
info.append("Fetching compass");
} else {
if (mCompass.getOrientations().getStatus() != CarValue.STATUS_SUCCESS) {
- info.append("Compass unavailable.");
+ info.append("Compass N/A.");
} else {
info.append("Compass: ");
appendFloatList(info, mCompass.getOrientations().getValue());
}
}
- canvas.drawText(info.toString(), visibleArea.centerX(), verticalPos, mCarInfoPaint);
+ canvas.drawText(info.toString(), LEFT_MARGIN, verticalPos, mCarInfoPaint);
verticalPos += height;
// Prepare text for Location
@@ -588,13 +493,13 @@
info.append("Fetching location");
} else {
if (mCarHardwareLocation.getLocation().getStatus() != CarValue.STATUS_SUCCESS) {
- info.append("Car Hardware Location unavailable");
+ info.append("Car Hardware Location N/A");
} else {
info.append("Car Hardware location: ");
info.append(mCarHardwareLocation.getLocation().getValue().toString());
}
}
- canvas.drawText(info.toString(), visibleArea.centerX(), verticalPos, mCarInfoPaint);
+ canvas.drawText(info.toString(), LEFT_MARGIN, verticalPos, mCarInfoPaint);
}
}
diff --git a/car/app/app-samples/showcase/common/src/main/res/drawable/info_gm_grey_24dp.png b/car/app/app-samples/showcase/common/src/main/res/drawable/info_gm_grey_24dp.png
new file mode 100644
index 0000000..6aeeb2f
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/res/drawable/info_gm_grey_24dp.png
Binary files differ
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/CarAppPermission.java b/car/app/app/src/main/java/androidx/car/app/CarAppPermission.java
index 4d5fba8..9349bb1 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarAppPermission.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarAppPermission.java
@@ -58,7 +58,7 @@
*
* <p>This permission should only be declared by apps that belong to one of the categories that
* allow using the navigation templates. See
- * <a href="https://developer.android.com/training/cars/navigation#access-navigation-templates">the
+ * <a href="https://developer.android.com/training/cars/apps/navigation#access-navigation-templates">the
* documentation</a> for the list of such categories. An app not in one of those categories
* requesting this permission may be rejected upon submission to the Play Store. See
* {@link CarAppService} for how to declare your app's category.
@@ -74,7 +74,7 @@
*
* <p>This permission should only be declared by apps that belong to one of the categories that
* allow using the map templates. See
- * <a href="https://developer.android.com/training/cars/navigation#access-map-template">the
+ * <a href="https://developer.android.com/training/cars/apps/poi#access-map-template">the
* documentation</a> for the list of such categories. An app not in one of those categories
* requesting this permission may be rejected upon submission to the Play Store. See
* {@link CarAppService} for how to declare your app's category.
diff --git a/car/app/app/src/main/java/androidx/car/app/CarAppService.java b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
index 572ea9c..c54c35e 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarAppService.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
@@ -73,7 +73,7 @@
* }</pre>
*
* <p>For a list of all the supported categories see
- * <a href="https://developer.android.com/training/cars/navigation#supported-app-categories">Supported App Categories</a>.
+ * <a href="https://developer.android.com/training/cars/apps#supported-app-categories">Supported App Categories</a>.
*
* <h4>Accessing Location</h4>
*
@@ -191,8 +191,8 @@
* don't hold the aforementioned permission (for example, Android Auto and Android
* Automotive OS hosts below API level 31), by allow-listing the signatures of those hosts.
*
- * <p>Please refer to {@link androidx.car.app.R.array#hosts_allowlist_sample} to obtain a list
- * of package names and signatures that should be allow-listed by default.
+ * <p>Refer to {@code androidx.car.app.R.array.hosts_allowlist_sample} to obtain a
+ * list of package names and signatures that should be allow-listed by default.
*
* <p>It is also advised to allow connections from unknown hosts in debug builds to facilitate
* debugging and testing.
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/collection/collection2/api/1.0.0.txt b/collection/collection2/api/1.0.0.txt
new file mode 100644
index 0000000..897ad4e
--- /dev/null
+++ b/collection/collection2/api/1.0.0.txt
@@ -0,0 +1,170 @@
+// Signature format: 3.0
+package androidx.collection {
+
+ public class ArrayMap<K, V> extends androidx.collection.SimpleArrayMap<K,V> implements java.util.Map<K,V> {
+ ctor public ArrayMap();
+ ctor public ArrayMap(int);
+ ctor public ArrayMap(androidx.collection.SimpleArrayMap!);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public java.util.Set<java.util.Map.Entry<K!,V!>!>! entrySet();
+ method public java.util.Set<K!>! keySet();
+ method public void putAll(java.util.Map<? extends K,? extends V>!);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public java.util.Collection<V!>! values();
+ }
+
+ public final class ArraySet<E> implements java.util.Collection<E> java.util.Set<E> {
+ ctor public ArraySet();
+ ctor public ArraySet(int);
+ ctor public ArraySet(androidx.collection.ArraySet<E!>?);
+ ctor public ArraySet(java.util.Collection<E!>?);
+ method public boolean add(E?);
+ method public void addAll(androidx.collection.ArraySet<? extends E>);
+ method public boolean addAll(java.util.Collection<? extends E>);
+ method public void clear();
+ method public boolean contains(Object?);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public void ensureCapacity(int);
+ method public int indexOf(Object?);
+ method public boolean isEmpty();
+ method public java.util.Iterator<E!>! iterator();
+ method public boolean remove(Object?);
+ method public boolean removeAll(androidx.collection.ArraySet<? extends E>);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public E! removeAt(int);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public int size();
+ method public Object![] toArray();
+ method public <T> T![] toArray(T![]);
+ method public E? valueAt(int);
+ }
+
+ public final class CircularArray<E> {
+ ctor public CircularArray();
+ ctor public CircularArray(int);
+ method public void addFirst(E!);
+ method public void addLast(E!);
+ method public void clear();
+ method public E! get(int);
+ method public E! getFirst();
+ method public E! getLast();
+ method public boolean isEmpty();
+ method public E! popFirst();
+ method public E! popLast();
+ method public void removeFromEnd(int);
+ method public void removeFromStart(int);
+ method public int size();
+ }
+
+ public final class CircularIntArray {
+ ctor public CircularIntArray();
+ ctor public CircularIntArray(int);
+ method public void addFirst(int);
+ method public void addLast(int);
+ method public void clear();
+ method public int get(int);
+ method public int getFirst();
+ method public int getLast();
+ method public boolean isEmpty();
+ method public int popFirst();
+ method public int popLast();
+ method public void removeFromEnd(int);
+ method public void removeFromStart(int);
+ method public int size();
+ }
+
+ public class LongSparseArray<E> implements java.lang.Cloneable {
+ ctor public LongSparseArray();
+ ctor public LongSparseArray(int);
+ method public void append(long, E!);
+ method public void clear();
+ method public androidx.collection.LongSparseArray<E!>! clone();
+ method public boolean containsKey(long);
+ method public boolean containsValue(E!);
+ method public void delete(long);
+ method public E? get(long);
+ method public E! get(long, E!);
+ method public int indexOfKey(long);
+ method public int indexOfValue(E!);
+ method public boolean isEmpty();
+ method public long keyAt(int);
+ method public void put(long, E!);
+ method public void putAll(androidx.collection.LongSparseArray<? extends E>);
+ method public void remove(long);
+ method public void removeAt(int);
+ method public void setValueAt(int, E!);
+ method public int size();
+ method public E! valueAt(int);
+ }
+
+ public class LruCache<K, V> {
+ ctor public LruCache(int);
+ method protected V? create(K);
+ method public final int createCount();
+ method protected void entryRemoved(boolean, K, V, V?);
+ method public final void evictAll();
+ method public final int evictionCount();
+ method public final V? get(K);
+ method public final int hitCount();
+ method public final int maxSize();
+ method public final int missCount();
+ method public final V? put(K, V);
+ method public final int putCount();
+ method public final V? remove(K);
+ method public void resize(int);
+ method public final int size();
+ method protected int sizeOf(K, V);
+ method public final java.util.Map<K!,V!>! snapshot();
+ method public final String toString();
+ method public void trimToSize(int);
+ }
+
+ public class SimpleArrayMap<K, V> {
+ ctor public SimpleArrayMap();
+ ctor public SimpleArrayMap(int);
+ ctor public SimpleArrayMap(androidx.collection.SimpleArrayMap<K!,V!>!);
+ method public void clear();
+ method public boolean containsKey(Object?);
+ method public boolean containsValue(Object!);
+ method public void ensureCapacity(int);
+ method public V? get(Object!);
+ method public int indexOfKey(Object?);
+ method public boolean isEmpty();
+ method public K! keyAt(int);
+ method public V? put(K!, V!);
+ method public void putAll(androidx.collection.SimpleArrayMap<? extends K,? extends V>);
+ method public V? remove(Object!);
+ method public V! removeAt(int);
+ method public V! setValueAt(int, V!);
+ method public int size();
+ method public V! valueAt(int);
+ }
+
+ public class SparseArrayCompat<E> implements java.lang.Cloneable {
+ ctor public SparseArrayCompat();
+ ctor public SparseArrayCompat(int);
+ method public void append(int, E!);
+ method public void clear();
+ method public androidx.collection.SparseArrayCompat<E!>! clone();
+ method public boolean containsKey(int);
+ method public boolean containsValue(E!);
+ method public void delete(int);
+ method public E? get(int);
+ method public E! get(int, E!);
+ method public int indexOfKey(int);
+ method public int indexOfValue(E!);
+ method public boolean isEmpty();
+ method public int keyAt(int);
+ method public void put(int, E!);
+ method public void putAll(androidx.collection.SparseArrayCompat<? extends E>);
+ method public void remove(int);
+ method public void removeAt(int);
+ method public void removeAtRange(int, int);
+ method public void setValueAt(int, E!);
+ method public int size();
+ method public E! valueAt(int);
+ }
+
+}
+
diff --git a/collection/collection2/api/1.1.0-beta01.txt b/collection/collection2/api/1.1.0-beta01.txt
new file mode 100644
index 0000000..222ad00
--- /dev/null
+++ b/collection/collection2/api/1.1.0-beta01.txt
@@ -0,0 +1,183 @@
+// Signature format: 3.0
+package androidx.collection {
+
+ public class ArrayMap<K, V> extends androidx.collection.SimpleArrayMap<K,V> implements java.util.Map<K,V> {
+ ctor public ArrayMap();
+ ctor public ArrayMap(int);
+ ctor public ArrayMap(androidx.collection.SimpleArrayMap!);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public java.util.Set<java.util.Map.Entry<K!,V!>!>! entrySet();
+ method public java.util.Set<K!>! keySet();
+ method public void putAll(java.util.Map<? extends K,? extends V>!);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public java.util.Collection<V!>! values();
+ }
+
+ public final class ArraySet<E> implements java.util.Collection<E> java.util.Set<E> {
+ ctor public ArraySet();
+ ctor public ArraySet(int);
+ ctor public ArraySet(androidx.collection.ArraySet<E!>?);
+ ctor public ArraySet(java.util.Collection<E!>?);
+ method public boolean add(E?);
+ method public void addAll(androidx.collection.ArraySet<? extends E>);
+ method public boolean addAll(java.util.Collection<? extends E>);
+ method public void clear();
+ method public boolean contains(Object?);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public void ensureCapacity(int);
+ method public int indexOf(Object?);
+ method public boolean isEmpty();
+ method public java.util.Iterator<E!>! iterator();
+ method public boolean remove(Object?);
+ method public boolean removeAll(androidx.collection.ArraySet<? extends E>);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public E! removeAt(int);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public int size();
+ method public Object![] toArray();
+ method public <T> T![] toArray(T![]);
+ method public E? valueAt(int);
+ }
+
+ public final class CircularArray<E> {
+ ctor public CircularArray();
+ ctor public CircularArray(int);
+ method public void addFirst(E!);
+ method public void addLast(E!);
+ method public void clear();
+ method public E! get(int);
+ method public E! getFirst();
+ method public E! getLast();
+ method public boolean isEmpty();
+ method public E! popFirst();
+ method public E! popLast();
+ method public void removeFromEnd(int);
+ method public void removeFromStart(int);
+ method public int size();
+ }
+
+ public final class CircularIntArray {
+ ctor public CircularIntArray();
+ ctor public CircularIntArray(int);
+ method public void addFirst(int);
+ method public void addLast(int);
+ method public void clear();
+ method public int get(int);
+ method public int getFirst();
+ method public int getLast();
+ method public boolean isEmpty();
+ method public int popFirst();
+ method public int popLast();
+ method public void removeFromEnd(int);
+ method public void removeFromStart(int);
+ method public int size();
+ }
+
+ public class LongSparseArray<E> implements java.lang.Cloneable {
+ ctor public LongSparseArray();
+ ctor public LongSparseArray(int);
+ method public void append(long, E!);
+ method public void clear();
+ method public androidx.collection.LongSparseArray<E!>! clone();
+ method public boolean containsKey(long);
+ method public boolean containsValue(E!);
+ method @Deprecated public void delete(long);
+ method public E? get(long);
+ method public E! get(long, E!);
+ method public int indexOfKey(long);
+ method public int indexOfValue(E!);
+ method public boolean isEmpty();
+ method public long keyAt(int);
+ method public void put(long, E!);
+ method public void putAll(androidx.collection.LongSparseArray<? extends E>);
+ method public E? putIfAbsent(long, E!);
+ method public void remove(long);
+ method public boolean remove(long, Object!);
+ method public void removeAt(int);
+ method public E? replace(long, E!);
+ method public boolean replace(long, E!, E!);
+ method public void setValueAt(int, E!);
+ method public int size();
+ method public E! valueAt(int);
+ }
+
+ public class LruCache<K, V> {
+ ctor public LruCache(int);
+ method protected V? create(K);
+ method public final int createCount();
+ method protected void entryRemoved(boolean, K, V, V?);
+ method public final void evictAll();
+ method public final int evictionCount();
+ method public final V? get(K);
+ method public final int hitCount();
+ method public final int maxSize();
+ method public final int missCount();
+ method public final V? put(K, V);
+ method public final int putCount();
+ method public final V? remove(K);
+ method public void resize(int);
+ method public final int size();
+ method protected int sizeOf(K, V);
+ method public final java.util.Map<K!,V!>! snapshot();
+ method public final String toString();
+ method public void trimToSize(int);
+ }
+
+ public class SimpleArrayMap<K, V> {
+ ctor public SimpleArrayMap();
+ ctor public SimpleArrayMap(int);
+ ctor public SimpleArrayMap(androidx.collection.SimpleArrayMap<K!,V!>!);
+ method public void clear();
+ method public boolean containsKey(Object?);
+ method public boolean containsValue(Object!);
+ method public void ensureCapacity(int);
+ method public V? get(Object!);
+ method public V! getOrDefault(Object!, V!);
+ method public int indexOfKey(Object?);
+ method public boolean isEmpty();
+ method public K! keyAt(int);
+ method public V? put(K!, V!);
+ method public void putAll(androidx.collection.SimpleArrayMap<? extends K,? extends V>);
+ method public V? putIfAbsent(K!, V!);
+ method public V? remove(Object!);
+ method public boolean remove(Object!, Object!);
+ method public V! removeAt(int);
+ method public V? replace(K!, V!);
+ method public boolean replace(K!, V!, V!);
+ method public V! setValueAt(int, V!);
+ method public int size();
+ method public V! valueAt(int);
+ }
+
+ public class SparseArrayCompat<E> implements java.lang.Cloneable {
+ ctor public SparseArrayCompat();
+ ctor public SparseArrayCompat(int);
+ method public void append(int, E!);
+ method public void clear();
+ method public androidx.collection.SparseArrayCompat<E!>! clone();
+ method public boolean containsKey(int);
+ method public boolean containsValue(E!);
+ method @Deprecated public void delete(int);
+ method public E? get(int);
+ method public E! get(int, E!);
+ method public int indexOfKey(int);
+ method public int indexOfValue(E!);
+ method public boolean isEmpty();
+ method public int keyAt(int);
+ method public void put(int, E!);
+ method public void putAll(androidx.collection.SparseArrayCompat<? extends E>);
+ method public E? putIfAbsent(int, E!);
+ method public void remove(int);
+ method public boolean remove(int, Object!);
+ method public void removeAt(int);
+ method public void removeAtRange(int, int);
+ method public E? replace(int, E!);
+ method public boolean replace(int, E!, E!);
+ method public void setValueAt(int, E!);
+ method public int size();
+ method public E! valueAt(int);
+ }
+
+}
+
diff --git a/collection/collection2/api/1.1.0-beta02.txt b/collection/collection2/api/1.1.0-beta02.txt
new file mode 100644
index 0000000..222ad00
--- /dev/null
+++ b/collection/collection2/api/1.1.0-beta02.txt
@@ -0,0 +1,183 @@
+// Signature format: 3.0
+package androidx.collection {
+
+ public class ArrayMap<K, V> extends androidx.collection.SimpleArrayMap<K,V> implements java.util.Map<K,V> {
+ ctor public ArrayMap();
+ ctor public ArrayMap(int);
+ ctor public ArrayMap(androidx.collection.SimpleArrayMap!);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public java.util.Set<java.util.Map.Entry<K!,V!>!>! entrySet();
+ method public java.util.Set<K!>! keySet();
+ method public void putAll(java.util.Map<? extends K,? extends V>!);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public java.util.Collection<V!>! values();
+ }
+
+ public final class ArraySet<E> implements java.util.Collection<E> java.util.Set<E> {
+ ctor public ArraySet();
+ ctor public ArraySet(int);
+ ctor public ArraySet(androidx.collection.ArraySet<E!>?);
+ ctor public ArraySet(java.util.Collection<E!>?);
+ method public boolean add(E?);
+ method public void addAll(androidx.collection.ArraySet<? extends E>);
+ method public boolean addAll(java.util.Collection<? extends E>);
+ method public void clear();
+ method public boolean contains(Object?);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public void ensureCapacity(int);
+ method public int indexOf(Object?);
+ method public boolean isEmpty();
+ method public java.util.Iterator<E!>! iterator();
+ method public boolean remove(Object?);
+ method public boolean removeAll(androidx.collection.ArraySet<? extends E>);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public E! removeAt(int);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public int size();
+ method public Object![] toArray();
+ method public <T> T![] toArray(T![]);
+ method public E? valueAt(int);
+ }
+
+ public final class CircularArray<E> {
+ ctor public CircularArray();
+ ctor public CircularArray(int);
+ method public void addFirst(E!);
+ method public void addLast(E!);
+ method public void clear();
+ method public E! get(int);
+ method public E! getFirst();
+ method public E! getLast();
+ method public boolean isEmpty();
+ method public E! popFirst();
+ method public E! popLast();
+ method public void removeFromEnd(int);
+ method public void removeFromStart(int);
+ method public int size();
+ }
+
+ public final class CircularIntArray {
+ ctor public CircularIntArray();
+ ctor public CircularIntArray(int);
+ method public void addFirst(int);
+ method public void addLast(int);
+ method public void clear();
+ method public int get(int);
+ method public int getFirst();
+ method public int getLast();
+ method public boolean isEmpty();
+ method public int popFirst();
+ method public int popLast();
+ method public void removeFromEnd(int);
+ method public void removeFromStart(int);
+ method public int size();
+ }
+
+ public class LongSparseArray<E> implements java.lang.Cloneable {
+ ctor public LongSparseArray();
+ ctor public LongSparseArray(int);
+ method public void append(long, E!);
+ method public void clear();
+ method public androidx.collection.LongSparseArray<E!>! clone();
+ method public boolean containsKey(long);
+ method public boolean containsValue(E!);
+ method @Deprecated public void delete(long);
+ method public E? get(long);
+ method public E! get(long, E!);
+ method public int indexOfKey(long);
+ method public int indexOfValue(E!);
+ method public boolean isEmpty();
+ method public long keyAt(int);
+ method public void put(long, E!);
+ method public void putAll(androidx.collection.LongSparseArray<? extends E>);
+ method public E? putIfAbsent(long, E!);
+ method public void remove(long);
+ method public boolean remove(long, Object!);
+ method public void removeAt(int);
+ method public E? replace(long, E!);
+ method public boolean replace(long, E!, E!);
+ method public void setValueAt(int, E!);
+ method public int size();
+ method public E! valueAt(int);
+ }
+
+ public class LruCache<K, V> {
+ ctor public LruCache(int);
+ method protected V? create(K);
+ method public final int createCount();
+ method protected void entryRemoved(boolean, K, V, V?);
+ method public final void evictAll();
+ method public final int evictionCount();
+ method public final V? get(K);
+ method public final int hitCount();
+ method public final int maxSize();
+ method public final int missCount();
+ method public final V? put(K, V);
+ method public final int putCount();
+ method public final V? remove(K);
+ method public void resize(int);
+ method public final int size();
+ method protected int sizeOf(K, V);
+ method public final java.util.Map<K!,V!>! snapshot();
+ method public final String toString();
+ method public void trimToSize(int);
+ }
+
+ public class SimpleArrayMap<K, V> {
+ ctor public SimpleArrayMap();
+ ctor public SimpleArrayMap(int);
+ ctor public SimpleArrayMap(androidx.collection.SimpleArrayMap<K!,V!>!);
+ method public void clear();
+ method public boolean containsKey(Object?);
+ method public boolean containsValue(Object!);
+ method public void ensureCapacity(int);
+ method public V? get(Object!);
+ method public V! getOrDefault(Object!, V!);
+ method public int indexOfKey(Object?);
+ method public boolean isEmpty();
+ method public K! keyAt(int);
+ method public V? put(K!, V!);
+ method public void putAll(androidx.collection.SimpleArrayMap<? extends K,? extends V>);
+ method public V? putIfAbsent(K!, V!);
+ method public V? remove(Object!);
+ method public boolean remove(Object!, Object!);
+ method public V! removeAt(int);
+ method public V? replace(K!, V!);
+ method public boolean replace(K!, V!, V!);
+ method public V! setValueAt(int, V!);
+ method public int size();
+ method public V! valueAt(int);
+ }
+
+ public class SparseArrayCompat<E> implements java.lang.Cloneable {
+ ctor public SparseArrayCompat();
+ ctor public SparseArrayCompat(int);
+ method public void append(int, E!);
+ method public void clear();
+ method public androidx.collection.SparseArrayCompat<E!>! clone();
+ method public boolean containsKey(int);
+ method public boolean containsValue(E!);
+ method @Deprecated public void delete(int);
+ method public E? get(int);
+ method public E! get(int, E!);
+ method public int indexOfKey(int);
+ method public int indexOfValue(E!);
+ method public boolean isEmpty();
+ method public int keyAt(int);
+ method public void put(int, E!);
+ method public void putAll(androidx.collection.SparseArrayCompat<? extends E>);
+ method public E? putIfAbsent(int, E!);
+ method public void remove(int);
+ method public boolean remove(int, Object!);
+ method public void removeAt(int);
+ method public void removeAtRange(int, int);
+ method public E? replace(int, E!);
+ method public boolean replace(int, E!, E!);
+ method public void setValueAt(int, E!);
+ method public int size();
+ method public E! valueAt(int);
+ }
+
+}
+
diff --git a/collection/collection2/api/1.1.0-rc01.txt b/collection/collection2/api/1.1.0-rc01.txt
new file mode 100644
index 0000000..222ad00
--- /dev/null
+++ b/collection/collection2/api/1.1.0-rc01.txt
@@ -0,0 +1,183 @@
+// Signature format: 3.0
+package androidx.collection {
+
+ public class ArrayMap<K, V> extends androidx.collection.SimpleArrayMap<K,V> implements java.util.Map<K,V> {
+ ctor public ArrayMap();
+ ctor public ArrayMap(int);
+ ctor public ArrayMap(androidx.collection.SimpleArrayMap!);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public java.util.Set<java.util.Map.Entry<K!,V!>!>! entrySet();
+ method public java.util.Set<K!>! keySet();
+ method public void putAll(java.util.Map<? extends K,? extends V>!);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public java.util.Collection<V!>! values();
+ }
+
+ public final class ArraySet<E> implements java.util.Collection<E> java.util.Set<E> {
+ ctor public ArraySet();
+ ctor public ArraySet(int);
+ ctor public ArraySet(androidx.collection.ArraySet<E!>?);
+ ctor public ArraySet(java.util.Collection<E!>?);
+ method public boolean add(E?);
+ method public void addAll(androidx.collection.ArraySet<? extends E>);
+ method public boolean addAll(java.util.Collection<? extends E>);
+ method public void clear();
+ method public boolean contains(Object?);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public void ensureCapacity(int);
+ method public int indexOf(Object?);
+ method public boolean isEmpty();
+ method public java.util.Iterator<E!>! iterator();
+ method public boolean remove(Object?);
+ method public boolean removeAll(androidx.collection.ArraySet<? extends E>);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public E! removeAt(int);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public int size();
+ method public Object![] toArray();
+ method public <T> T![] toArray(T![]);
+ method public E? valueAt(int);
+ }
+
+ public final class CircularArray<E> {
+ ctor public CircularArray();
+ ctor public CircularArray(int);
+ method public void addFirst(E!);
+ method public void addLast(E!);
+ method public void clear();
+ method public E! get(int);
+ method public E! getFirst();
+ method public E! getLast();
+ method public boolean isEmpty();
+ method public E! popFirst();
+ method public E! popLast();
+ method public void removeFromEnd(int);
+ method public void removeFromStart(int);
+ method public int size();
+ }
+
+ public final class CircularIntArray {
+ ctor public CircularIntArray();
+ ctor public CircularIntArray(int);
+ method public void addFirst(int);
+ method public void addLast(int);
+ method public void clear();
+ method public int get(int);
+ method public int getFirst();
+ method public int getLast();
+ method public boolean isEmpty();
+ method public int popFirst();
+ method public int popLast();
+ method public void removeFromEnd(int);
+ method public void removeFromStart(int);
+ method public int size();
+ }
+
+ public class LongSparseArray<E> implements java.lang.Cloneable {
+ ctor public LongSparseArray();
+ ctor public LongSparseArray(int);
+ method public void append(long, E!);
+ method public void clear();
+ method public androidx.collection.LongSparseArray<E!>! clone();
+ method public boolean containsKey(long);
+ method public boolean containsValue(E!);
+ method @Deprecated public void delete(long);
+ method public E? get(long);
+ method public E! get(long, E!);
+ method public int indexOfKey(long);
+ method public int indexOfValue(E!);
+ method public boolean isEmpty();
+ method public long keyAt(int);
+ method public void put(long, E!);
+ method public void putAll(androidx.collection.LongSparseArray<? extends E>);
+ method public E? putIfAbsent(long, E!);
+ method public void remove(long);
+ method public boolean remove(long, Object!);
+ method public void removeAt(int);
+ method public E? replace(long, E!);
+ method public boolean replace(long, E!, E!);
+ method public void setValueAt(int, E!);
+ method public int size();
+ method public E! valueAt(int);
+ }
+
+ public class LruCache<K, V> {
+ ctor public LruCache(int);
+ method protected V? create(K);
+ method public final int createCount();
+ method protected void entryRemoved(boolean, K, V, V?);
+ method public final void evictAll();
+ method public final int evictionCount();
+ method public final V? get(K);
+ method public final int hitCount();
+ method public final int maxSize();
+ method public final int missCount();
+ method public final V? put(K, V);
+ method public final int putCount();
+ method public final V? remove(K);
+ method public void resize(int);
+ method public final int size();
+ method protected int sizeOf(K, V);
+ method public final java.util.Map<K!,V!>! snapshot();
+ method public final String toString();
+ method public void trimToSize(int);
+ }
+
+ public class SimpleArrayMap<K, V> {
+ ctor public SimpleArrayMap();
+ ctor public SimpleArrayMap(int);
+ ctor public SimpleArrayMap(androidx.collection.SimpleArrayMap<K!,V!>!);
+ method public void clear();
+ method public boolean containsKey(Object?);
+ method public boolean containsValue(Object!);
+ method public void ensureCapacity(int);
+ method public V? get(Object!);
+ method public V! getOrDefault(Object!, V!);
+ method public int indexOfKey(Object?);
+ method public boolean isEmpty();
+ method public K! keyAt(int);
+ method public V? put(K!, V!);
+ method public void putAll(androidx.collection.SimpleArrayMap<? extends K,? extends V>);
+ method public V? putIfAbsent(K!, V!);
+ method public V? remove(Object!);
+ method public boolean remove(Object!, Object!);
+ method public V! removeAt(int);
+ method public V? replace(K!, V!);
+ method public boolean replace(K!, V!, V!);
+ method public V! setValueAt(int, V!);
+ method public int size();
+ method public V! valueAt(int);
+ }
+
+ public class SparseArrayCompat<E> implements java.lang.Cloneable {
+ ctor public SparseArrayCompat();
+ ctor public SparseArrayCompat(int);
+ method public void append(int, E!);
+ method public void clear();
+ method public androidx.collection.SparseArrayCompat<E!>! clone();
+ method public boolean containsKey(int);
+ method public boolean containsValue(E!);
+ method @Deprecated public void delete(int);
+ method public E? get(int);
+ method public E! get(int, E!);
+ method public int indexOfKey(int);
+ method public int indexOfValue(E!);
+ method public boolean isEmpty();
+ method public int keyAt(int);
+ method public void put(int, E!);
+ method public void putAll(androidx.collection.SparseArrayCompat<? extends E>);
+ method public E? putIfAbsent(int, E!);
+ method public void remove(int);
+ method public boolean remove(int, Object!);
+ method public void removeAt(int);
+ method public void removeAtRange(int, int);
+ method public E? replace(int, E!);
+ method public boolean replace(int, E!, E!);
+ method public void setValueAt(int, E!);
+ method public int size();
+ method public E! valueAt(int);
+ }
+
+}
+
diff --git a/collection/collection2/api/current.ignore b/collection/collection2/api/current.ignore
new file mode 100644
index 0000000..805bf2c
--- /dev/null
+++ b/collection/collection2/api/current.ignore
@@ -0,0 +1,23 @@
+// Baseline format: 1.0
+ChangedSuperclass: androidx.collection.ArrayMap:
+ Class androidx.collection.ArrayMap superclass changed from androidx.collection.SimpleArrayMap to java.lang.Object
+
+
+RemovedClass: androidx.collection.ArraySet:
+ Removed class androidx.collection.ArraySet
+RemovedClass: androidx.collection.CircularArray:
+ Removed class androidx.collection.CircularArray
+RemovedClass: androidx.collection.CircularIntArray:
+ Removed class androidx.collection.CircularIntArray
+RemovedClass: androidx.collection.LongSparseArray:
+ Removed class androidx.collection.LongSparseArray
+RemovedClass: androidx.collection.LruCache:
+ Removed class androidx.collection.LruCache
+RemovedClass: androidx.collection.SimpleArrayMap:
+ Removed class androidx.collection.SimpleArrayMap
+RemovedClass: androidx.collection.SparseArrayCompat:
+ Removed class androidx.collection.SparseArrayCompat
+
+
+RemovedMethod: androidx.collection.ArrayMap#ArrayMap(androidx.collection.SimpleArrayMap):
+ Removed constructor androidx.collection.ArrayMap(androidx.collection.SimpleArrayMap)
diff --git a/collection/collection2/api/current.txt b/collection/collection2/api/current.txt
new file mode 100644
index 0000000..01eae79
--- /dev/null
+++ b/collection/collection2/api/current.txt
@@ -0,0 +1,22 @@
+// Signature format: 4.0
+package androidx.collection {
+
+ public class ArrayMap<K, V> implements java.util.Map<K,V> {
+ ctor public ArrayMap();
+ ctor public ArrayMap(int);
+ ctor public ArrayMap(SimpleArrayMap<K,V>);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public boolean containsKey(Object?);
+ method public boolean containsValue(Object);
+ method public java.util.Set<java.util.Map.Entry<K!,V!>!> entrySet();
+ method public V? get(Object);
+ method public java.util.Set<K!> keySet();
+ method public void putAll(java.util.Map<? extends K,? extends V>);
+ method public V? remove(Object);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public java.util.Collection<V!> values();
+ }
+
+}
+
diff --git a/collection/collection2/api/public_plus_experimental_1.0.0.txt b/collection/collection2/api/public_plus_experimental_1.0.0.txt
new file mode 100644
index 0000000..897ad4e
--- /dev/null
+++ b/collection/collection2/api/public_plus_experimental_1.0.0.txt
@@ -0,0 +1,170 @@
+// Signature format: 3.0
+package androidx.collection {
+
+ public class ArrayMap<K, V> extends androidx.collection.SimpleArrayMap<K,V> implements java.util.Map<K,V> {
+ ctor public ArrayMap();
+ ctor public ArrayMap(int);
+ ctor public ArrayMap(androidx.collection.SimpleArrayMap!);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public java.util.Set<java.util.Map.Entry<K!,V!>!>! entrySet();
+ method public java.util.Set<K!>! keySet();
+ method public void putAll(java.util.Map<? extends K,? extends V>!);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public java.util.Collection<V!>! values();
+ }
+
+ public final class ArraySet<E> implements java.util.Collection<E> java.util.Set<E> {
+ ctor public ArraySet();
+ ctor public ArraySet(int);
+ ctor public ArraySet(androidx.collection.ArraySet<E!>?);
+ ctor public ArraySet(java.util.Collection<E!>?);
+ method public boolean add(E?);
+ method public void addAll(androidx.collection.ArraySet<? extends E>);
+ method public boolean addAll(java.util.Collection<? extends E>);
+ method public void clear();
+ method public boolean contains(Object?);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public void ensureCapacity(int);
+ method public int indexOf(Object?);
+ method public boolean isEmpty();
+ method public java.util.Iterator<E!>! iterator();
+ method public boolean remove(Object?);
+ method public boolean removeAll(androidx.collection.ArraySet<? extends E>);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public E! removeAt(int);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public int size();
+ method public Object![] toArray();
+ method public <T> T![] toArray(T![]);
+ method public E? valueAt(int);
+ }
+
+ public final class CircularArray<E> {
+ ctor public CircularArray();
+ ctor public CircularArray(int);
+ method public void addFirst(E!);
+ method public void addLast(E!);
+ method public void clear();
+ method public E! get(int);
+ method public E! getFirst();
+ method public E! getLast();
+ method public boolean isEmpty();
+ method public E! popFirst();
+ method public E! popLast();
+ method public void removeFromEnd(int);
+ method public void removeFromStart(int);
+ method public int size();
+ }
+
+ public final class CircularIntArray {
+ ctor public CircularIntArray();
+ ctor public CircularIntArray(int);
+ method public void addFirst(int);
+ method public void addLast(int);
+ method public void clear();
+ method public int get(int);
+ method public int getFirst();
+ method public int getLast();
+ method public boolean isEmpty();
+ method public int popFirst();
+ method public int popLast();
+ method public void removeFromEnd(int);
+ method public void removeFromStart(int);
+ method public int size();
+ }
+
+ public class LongSparseArray<E> implements java.lang.Cloneable {
+ ctor public LongSparseArray();
+ ctor public LongSparseArray(int);
+ method public void append(long, E!);
+ method public void clear();
+ method public androidx.collection.LongSparseArray<E!>! clone();
+ method public boolean containsKey(long);
+ method public boolean containsValue(E!);
+ method public void delete(long);
+ method public E? get(long);
+ method public E! get(long, E!);
+ method public int indexOfKey(long);
+ method public int indexOfValue(E!);
+ method public boolean isEmpty();
+ method public long keyAt(int);
+ method public void put(long, E!);
+ method public void putAll(androidx.collection.LongSparseArray<? extends E>);
+ method public void remove(long);
+ method public void removeAt(int);
+ method public void setValueAt(int, E!);
+ method public int size();
+ method public E! valueAt(int);
+ }
+
+ public class LruCache<K, V> {
+ ctor public LruCache(int);
+ method protected V? create(K);
+ method public final int createCount();
+ method protected void entryRemoved(boolean, K, V, V?);
+ method public final void evictAll();
+ method public final int evictionCount();
+ method public final V? get(K);
+ method public final int hitCount();
+ method public final int maxSize();
+ method public final int missCount();
+ method public final V? put(K, V);
+ method public final int putCount();
+ method public final V? remove(K);
+ method public void resize(int);
+ method public final int size();
+ method protected int sizeOf(K, V);
+ method public final java.util.Map<K!,V!>! snapshot();
+ method public final String toString();
+ method public void trimToSize(int);
+ }
+
+ public class SimpleArrayMap<K, V> {
+ ctor public SimpleArrayMap();
+ ctor public SimpleArrayMap(int);
+ ctor public SimpleArrayMap(androidx.collection.SimpleArrayMap<K!,V!>!);
+ method public void clear();
+ method public boolean containsKey(Object?);
+ method public boolean containsValue(Object!);
+ method public void ensureCapacity(int);
+ method public V? get(Object!);
+ method public int indexOfKey(Object?);
+ method public boolean isEmpty();
+ method public K! keyAt(int);
+ method public V? put(K!, V!);
+ method public void putAll(androidx.collection.SimpleArrayMap<? extends K,? extends V>);
+ method public V? remove(Object!);
+ method public V! removeAt(int);
+ method public V! setValueAt(int, V!);
+ method public int size();
+ method public V! valueAt(int);
+ }
+
+ public class SparseArrayCompat<E> implements java.lang.Cloneable {
+ ctor public SparseArrayCompat();
+ ctor public SparseArrayCompat(int);
+ method public void append(int, E!);
+ method public void clear();
+ method public androidx.collection.SparseArrayCompat<E!>! clone();
+ method public boolean containsKey(int);
+ method public boolean containsValue(E!);
+ method public void delete(int);
+ method public E? get(int);
+ method public E! get(int, E!);
+ method public int indexOfKey(int);
+ method public int indexOfValue(E!);
+ method public boolean isEmpty();
+ method public int keyAt(int);
+ method public void put(int, E!);
+ method public void putAll(androidx.collection.SparseArrayCompat<? extends E>);
+ method public void remove(int);
+ method public void removeAt(int);
+ method public void removeAtRange(int, int);
+ method public void setValueAt(int, E!);
+ method public int size();
+ method public E! valueAt(int);
+ }
+
+}
+
diff --git a/collection/collection2/api/public_plus_experimental_1.1.0-beta01.txt b/collection/collection2/api/public_plus_experimental_1.1.0-beta01.txt
new file mode 100644
index 0000000..222ad00
--- /dev/null
+++ b/collection/collection2/api/public_plus_experimental_1.1.0-beta01.txt
@@ -0,0 +1,183 @@
+// Signature format: 3.0
+package androidx.collection {
+
+ public class ArrayMap<K, V> extends androidx.collection.SimpleArrayMap<K,V> implements java.util.Map<K,V> {
+ ctor public ArrayMap();
+ ctor public ArrayMap(int);
+ ctor public ArrayMap(androidx.collection.SimpleArrayMap!);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public java.util.Set<java.util.Map.Entry<K!,V!>!>! entrySet();
+ method public java.util.Set<K!>! keySet();
+ method public void putAll(java.util.Map<? extends K,? extends V>!);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public java.util.Collection<V!>! values();
+ }
+
+ public final class ArraySet<E> implements java.util.Collection<E> java.util.Set<E> {
+ ctor public ArraySet();
+ ctor public ArraySet(int);
+ ctor public ArraySet(androidx.collection.ArraySet<E!>?);
+ ctor public ArraySet(java.util.Collection<E!>?);
+ method public boolean add(E?);
+ method public void addAll(androidx.collection.ArraySet<? extends E>);
+ method public boolean addAll(java.util.Collection<? extends E>);
+ method public void clear();
+ method public boolean contains(Object?);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public void ensureCapacity(int);
+ method public int indexOf(Object?);
+ method public boolean isEmpty();
+ method public java.util.Iterator<E!>! iterator();
+ method public boolean remove(Object?);
+ method public boolean removeAll(androidx.collection.ArraySet<? extends E>);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public E! removeAt(int);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public int size();
+ method public Object![] toArray();
+ method public <T> T![] toArray(T![]);
+ method public E? valueAt(int);
+ }
+
+ public final class CircularArray<E> {
+ ctor public CircularArray();
+ ctor public CircularArray(int);
+ method public void addFirst(E!);
+ method public void addLast(E!);
+ method public void clear();
+ method public E! get(int);
+ method public E! getFirst();
+ method public E! getLast();
+ method public boolean isEmpty();
+ method public E! popFirst();
+ method public E! popLast();
+ method public void removeFromEnd(int);
+ method public void removeFromStart(int);
+ method public int size();
+ }
+
+ public final class CircularIntArray {
+ ctor public CircularIntArray();
+ ctor public CircularIntArray(int);
+ method public void addFirst(int);
+ method public void addLast(int);
+ method public void clear();
+ method public int get(int);
+ method public int getFirst();
+ method public int getLast();
+ method public boolean isEmpty();
+ method public int popFirst();
+ method public int popLast();
+ method public void removeFromEnd(int);
+ method public void removeFromStart(int);
+ method public int size();
+ }
+
+ public class LongSparseArray<E> implements java.lang.Cloneable {
+ ctor public LongSparseArray();
+ ctor public LongSparseArray(int);
+ method public void append(long, E!);
+ method public void clear();
+ method public androidx.collection.LongSparseArray<E!>! clone();
+ method public boolean containsKey(long);
+ method public boolean containsValue(E!);
+ method @Deprecated public void delete(long);
+ method public E? get(long);
+ method public E! get(long, E!);
+ method public int indexOfKey(long);
+ method public int indexOfValue(E!);
+ method public boolean isEmpty();
+ method public long keyAt(int);
+ method public void put(long, E!);
+ method public void putAll(androidx.collection.LongSparseArray<? extends E>);
+ method public E? putIfAbsent(long, E!);
+ method public void remove(long);
+ method public boolean remove(long, Object!);
+ method public void removeAt(int);
+ method public E? replace(long, E!);
+ method public boolean replace(long, E!, E!);
+ method public void setValueAt(int, E!);
+ method public int size();
+ method public E! valueAt(int);
+ }
+
+ public class LruCache<K, V> {
+ ctor public LruCache(int);
+ method protected V? create(K);
+ method public final int createCount();
+ method protected void entryRemoved(boolean, K, V, V?);
+ method public final void evictAll();
+ method public final int evictionCount();
+ method public final V? get(K);
+ method public final int hitCount();
+ method public final int maxSize();
+ method public final int missCount();
+ method public final V? put(K, V);
+ method public final int putCount();
+ method public final V? remove(K);
+ method public void resize(int);
+ method public final int size();
+ method protected int sizeOf(K, V);
+ method public final java.util.Map<K!,V!>! snapshot();
+ method public final String toString();
+ method public void trimToSize(int);
+ }
+
+ public class SimpleArrayMap<K, V> {
+ ctor public SimpleArrayMap();
+ ctor public SimpleArrayMap(int);
+ ctor public SimpleArrayMap(androidx.collection.SimpleArrayMap<K!,V!>!);
+ method public void clear();
+ method public boolean containsKey(Object?);
+ method public boolean containsValue(Object!);
+ method public void ensureCapacity(int);
+ method public V? get(Object!);
+ method public V! getOrDefault(Object!, V!);
+ method public int indexOfKey(Object?);
+ method public boolean isEmpty();
+ method public K! keyAt(int);
+ method public V? put(K!, V!);
+ method public void putAll(androidx.collection.SimpleArrayMap<? extends K,? extends V>);
+ method public V? putIfAbsent(K!, V!);
+ method public V? remove(Object!);
+ method public boolean remove(Object!, Object!);
+ method public V! removeAt(int);
+ method public V? replace(K!, V!);
+ method public boolean replace(K!, V!, V!);
+ method public V! setValueAt(int, V!);
+ method public int size();
+ method public V! valueAt(int);
+ }
+
+ public class SparseArrayCompat<E> implements java.lang.Cloneable {
+ ctor public SparseArrayCompat();
+ ctor public SparseArrayCompat(int);
+ method public void append(int, E!);
+ method public void clear();
+ method public androidx.collection.SparseArrayCompat<E!>! clone();
+ method public boolean containsKey(int);
+ method public boolean containsValue(E!);
+ method @Deprecated public void delete(int);
+ method public E? get(int);
+ method public E! get(int, E!);
+ method public int indexOfKey(int);
+ method public int indexOfValue(E!);
+ method public boolean isEmpty();
+ method public int keyAt(int);
+ method public void put(int, E!);
+ method public void putAll(androidx.collection.SparseArrayCompat<? extends E>);
+ method public E? putIfAbsent(int, E!);
+ method public void remove(int);
+ method public boolean remove(int, Object!);
+ method public void removeAt(int);
+ method public void removeAtRange(int, int);
+ method public E? replace(int, E!);
+ method public boolean replace(int, E!, E!);
+ method public void setValueAt(int, E!);
+ method public int size();
+ method public E! valueAt(int);
+ }
+
+}
+
diff --git a/collection/collection2/api/public_plus_experimental_1.1.0-rc01.txt b/collection/collection2/api/public_plus_experimental_1.1.0-rc01.txt
new file mode 100644
index 0000000..222ad00
--- /dev/null
+++ b/collection/collection2/api/public_plus_experimental_1.1.0-rc01.txt
@@ -0,0 +1,183 @@
+// Signature format: 3.0
+package androidx.collection {
+
+ public class ArrayMap<K, V> extends androidx.collection.SimpleArrayMap<K,V> implements java.util.Map<K,V> {
+ ctor public ArrayMap();
+ ctor public ArrayMap(int);
+ ctor public ArrayMap(androidx.collection.SimpleArrayMap!);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public java.util.Set<java.util.Map.Entry<K!,V!>!>! entrySet();
+ method public java.util.Set<K!>! keySet();
+ method public void putAll(java.util.Map<? extends K,? extends V>!);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public java.util.Collection<V!>! values();
+ }
+
+ public final class ArraySet<E> implements java.util.Collection<E> java.util.Set<E> {
+ ctor public ArraySet();
+ ctor public ArraySet(int);
+ ctor public ArraySet(androidx.collection.ArraySet<E!>?);
+ ctor public ArraySet(java.util.Collection<E!>?);
+ method public boolean add(E?);
+ method public void addAll(androidx.collection.ArraySet<? extends E>);
+ method public boolean addAll(java.util.Collection<? extends E>);
+ method public void clear();
+ method public boolean contains(Object?);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public void ensureCapacity(int);
+ method public int indexOf(Object?);
+ method public boolean isEmpty();
+ method public java.util.Iterator<E!>! iterator();
+ method public boolean remove(Object?);
+ method public boolean removeAll(androidx.collection.ArraySet<? extends E>);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public E! removeAt(int);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public int size();
+ method public Object![] toArray();
+ method public <T> T![] toArray(T![]);
+ method public E? valueAt(int);
+ }
+
+ public final class CircularArray<E> {
+ ctor public CircularArray();
+ ctor public CircularArray(int);
+ method public void addFirst(E!);
+ method public void addLast(E!);
+ method public void clear();
+ method public E! get(int);
+ method public E! getFirst();
+ method public E! getLast();
+ method public boolean isEmpty();
+ method public E! popFirst();
+ method public E! popLast();
+ method public void removeFromEnd(int);
+ method public void removeFromStart(int);
+ method public int size();
+ }
+
+ public final class CircularIntArray {
+ ctor public CircularIntArray();
+ ctor public CircularIntArray(int);
+ method public void addFirst(int);
+ method public void addLast(int);
+ method public void clear();
+ method public int get(int);
+ method public int getFirst();
+ method public int getLast();
+ method public boolean isEmpty();
+ method public int popFirst();
+ method public int popLast();
+ method public void removeFromEnd(int);
+ method public void removeFromStart(int);
+ method public int size();
+ }
+
+ public class LongSparseArray<E> implements java.lang.Cloneable {
+ ctor public LongSparseArray();
+ ctor public LongSparseArray(int);
+ method public void append(long, E!);
+ method public void clear();
+ method public androidx.collection.LongSparseArray<E!>! clone();
+ method public boolean containsKey(long);
+ method public boolean containsValue(E!);
+ method @Deprecated public void delete(long);
+ method public E? get(long);
+ method public E! get(long, E!);
+ method public int indexOfKey(long);
+ method public int indexOfValue(E!);
+ method public boolean isEmpty();
+ method public long keyAt(int);
+ method public void put(long, E!);
+ method public void putAll(androidx.collection.LongSparseArray<? extends E>);
+ method public E? putIfAbsent(long, E!);
+ method public void remove(long);
+ method public boolean remove(long, Object!);
+ method public void removeAt(int);
+ method public E? replace(long, E!);
+ method public boolean replace(long, E!, E!);
+ method public void setValueAt(int, E!);
+ method public int size();
+ method public E! valueAt(int);
+ }
+
+ public class LruCache<K, V> {
+ ctor public LruCache(int);
+ method protected V? create(K);
+ method public final int createCount();
+ method protected void entryRemoved(boolean, K, V, V?);
+ method public final void evictAll();
+ method public final int evictionCount();
+ method public final V? get(K);
+ method public final int hitCount();
+ method public final int maxSize();
+ method public final int missCount();
+ method public final V? put(K, V);
+ method public final int putCount();
+ method public final V? remove(K);
+ method public void resize(int);
+ method public final int size();
+ method protected int sizeOf(K, V);
+ method public final java.util.Map<K!,V!>! snapshot();
+ method public final String toString();
+ method public void trimToSize(int);
+ }
+
+ public class SimpleArrayMap<K, V> {
+ ctor public SimpleArrayMap();
+ ctor public SimpleArrayMap(int);
+ ctor public SimpleArrayMap(androidx.collection.SimpleArrayMap<K!,V!>!);
+ method public void clear();
+ method public boolean containsKey(Object?);
+ method public boolean containsValue(Object!);
+ method public void ensureCapacity(int);
+ method public V? get(Object!);
+ method public V! getOrDefault(Object!, V!);
+ method public int indexOfKey(Object?);
+ method public boolean isEmpty();
+ method public K! keyAt(int);
+ method public V? put(K!, V!);
+ method public void putAll(androidx.collection.SimpleArrayMap<? extends K,? extends V>);
+ method public V? putIfAbsent(K!, V!);
+ method public V? remove(Object!);
+ method public boolean remove(Object!, Object!);
+ method public V! removeAt(int);
+ method public V? replace(K!, V!);
+ method public boolean replace(K!, V!, V!);
+ method public V! setValueAt(int, V!);
+ method public int size();
+ method public V! valueAt(int);
+ }
+
+ public class SparseArrayCompat<E> implements java.lang.Cloneable {
+ ctor public SparseArrayCompat();
+ ctor public SparseArrayCompat(int);
+ method public void append(int, E!);
+ method public void clear();
+ method public androidx.collection.SparseArrayCompat<E!>! clone();
+ method public boolean containsKey(int);
+ method public boolean containsValue(E!);
+ method @Deprecated public void delete(int);
+ method public E? get(int);
+ method public E! get(int, E!);
+ method public int indexOfKey(int);
+ method public int indexOfValue(E!);
+ method public boolean isEmpty();
+ method public int keyAt(int);
+ method public void put(int, E!);
+ method public void putAll(androidx.collection.SparseArrayCompat<? extends E>);
+ method public E? putIfAbsent(int, E!);
+ method public void remove(int);
+ method public boolean remove(int, Object!);
+ method public void removeAt(int);
+ method public void removeAtRange(int, int);
+ method public E? replace(int, E!);
+ method public boolean replace(int, E!, E!);
+ method public void setValueAt(int, E!);
+ method public int size();
+ method public E! valueAt(int);
+ }
+
+}
+
diff --git a/collection/collection2/api/public_plus_experimental_current.txt b/collection/collection2/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..01eae79
--- /dev/null
+++ b/collection/collection2/api/public_plus_experimental_current.txt
@@ -0,0 +1,22 @@
+// Signature format: 4.0
+package androidx.collection {
+
+ public class ArrayMap<K, V> implements java.util.Map<K,V> {
+ ctor public ArrayMap();
+ ctor public ArrayMap(int);
+ ctor public ArrayMap(SimpleArrayMap<K,V>);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public boolean containsKey(Object?);
+ method public boolean containsValue(Object);
+ method public java.util.Set<java.util.Map.Entry<K!,V!>!> entrySet();
+ method public V? get(Object);
+ method public java.util.Set<K!> keySet();
+ method public void putAll(java.util.Map<? extends K,? extends V>);
+ method public V? remove(Object);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public java.util.Collection<V!> values();
+ }
+
+}
+
diff --git a/collection/collection2/api/restricted_1.0.0.txt b/collection/collection2/api/restricted_1.0.0.txt
new file mode 100644
index 0000000..897ad4e
--- /dev/null
+++ b/collection/collection2/api/restricted_1.0.0.txt
@@ -0,0 +1,170 @@
+// Signature format: 3.0
+package androidx.collection {
+
+ public class ArrayMap<K, V> extends androidx.collection.SimpleArrayMap<K,V> implements java.util.Map<K,V> {
+ ctor public ArrayMap();
+ ctor public ArrayMap(int);
+ ctor public ArrayMap(androidx.collection.SimpleArrayMap!);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public java.util.Set<java.util.Map.Entry<K!,V!>!>! entrySet();
+ method public java.util.Set<K!>! keySet();
+ method public void putAll(java.util.Map<? extends K,? extends V>!);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public java.util.Collection<V!>! values();
+ }
+
+ public final class ArraySet<E> implements java.util.Collection<E> java.util.Set<E> {
+ ctor public ArraySet();
+ ctor public ArraySet(int);
+ ctor public ArraySet(androidx.collection.ArraySet<E!>?);
+ ctor public ArraySet(java.util.Collection<E!>?);
+ method public boolean add(E?);
+ method public void addAll(androidx.collection.ArraySet<? extends E>);
+ method public boolean addAll(java.util.Collection<? extends E>);
+ method public void clear();
+ method public boolean contains(Object?);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public void ensureCapacity(int);
+ method public int indexOf(Object?);
+ method public boolean isEmpty();
+ method public java.util.Iterator<E!>! iterator();
+ method public boolean remove(Object?);
+ method public boolean removeAll(androidx.collection.ArraySet<? extends E>);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public E! removeAt(int);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public int size();
+ method public Object![] toArray();
+ method public <T> T![] toArray(T![]);
+ method public E? valueAt(int);
+ }
+
+ public final class CircularArray<E> {
+ ctor public CircularArray();
+ ctor public CircularArray(int);
+ method public void addFirst(E!);
+ method public void addLast(E!);
+ method public void clear();
+ method public E! get(int);
+ method public E! getFirst();
+ method public E! getLast();
+ method public boolean isEmpty();
+ method public E! popFirst();
+ method public E! popLast();
+ method public void removeFromEnd(int);
+ method public void removeFromStart(int);
+ method public int size();
+ }
+
+ public final class CircularIntArray {
+ ctor public CircularIntArray();
+ ctor public CircularIntArray(int);
+ method public void addFirst(int);
+ method public void addLast(int);
+ method public void clear();
+ method public int get(int);
+ method public int getFirst();
+ method public int getLast();
+ method public boolean isEmpty();
+ method public int popFirst();
+ method public int popLast();
+ method public void removeFromEnd(int);
+ method public void removeFromStart(int);
+ method public int size();
+ }
+
+ public class LongSparseArray<E> implements java.lang.Cloneable {
+ ctor public LongSparseArray();
+ ctor public LongSparseArray(int);
+ method public void append(long, E!);
+ method public void clear();
+ method public androidx.collection.LongSparseArray<E!>! clone();
+ method public boolean containsKey(long);
+ method public boolean containsValue(E!);
+ method public void delete(long);
+ method public E? get(long);
+ method public E! get(long, E!);
+ method public int indexOfKey(long);
+ method public int indexOfValue(E!);
+ method public boolean isEmpty();
+ method public long keyAt(int);
+ method public void put(long, E!);
+ method public void putAll(androidx.collection.LongSparseArray<? extends E>);
+ method public void remove(long);
+ method public void removeAt(int);
+ method public void setValueAt(int, E!);
+ method public int size();
+ method public E! valueAt(int);
+ }
+
+ public class LruCache<K, V> {
+ ctor public LruCache(int);
+ method protected V? create(K);
+ method public final int createCount();
+ method protected void entryRemoved(boolean, K, V, V?);
+ method public final void evictAll();
+ method public final int evictionCount();
+ method public final V? get(K);
+ method public final int hitCount();
+ method public final int maxSize();
+ method public final int missCount();
+ method public final V? put(K, V);
+ method public final int putCount();
+ method public final V? remove(K);
+ method public void resize(int);
+ method public final int size();
+ method protected int sizeOf(K, V);
+ method public final java.util.Map<K!,V!>! snapshot();
+ method public final String toString();
+ method public void trimToSize(int);
+ }
+
+ public class SimpleArrayMap<K, V> {
+ ctor public SimpleArrayMap();
+ ctor public SimpleArrayMap(int);
+ ctor public SimpleArrayMap(androidx.collection.SimpleArrayMap<K!,V!>!);
+ method public void clear();
+ method public boolean containsKey(Object?);
+ method public boolean containsValue(Object!);
+ method public void ensureCapacity(int);
+ method public V? get(Object!);
+ method public int indexOfKey(Object?);
+ method public boolean isEmpty();
+ method public K! keyAt(int);
+ method public V? put(K!, V!);
+ method public void putAll(androidx.collection.SimpleArrayMap<? extends K,? extends V>);
+ method public V? remove(Object!);
+ method public V! removeAt(int);
+ method public V! setValueAt(int, V!);
+ method public int size();
+ method public V! valueAt(int);
+ }
+
+ public class SparseArrayCompat<E> implements java.lang.Cloneable {
+ ctor public SparseArrayCompat();
+ ctor public SparseArrayCompat(int);
+ method public void append(int, E!);
+ method public void clear();
+ method public androidx.collection.SparseArrayCompat<E!>! clone();
+ method public boolean containsKey(int);
+ method public boolean containsValue(E!);
+ method public void delete(int);
+ method public E? get(int);
+ method public E! get(int, E!);
+ method public int indexOfKey(int);
+ method public int indexOfValue(E!);
+ method public boolean isEmpty();
+ method public int keyAt(int);
+ method public void put(int, E!);
+ method public void putAll(androidx.collection.SparseArrayCompat<? extends E>);
+ method public void remove(int);
+ method public void removeAt(int);
+ method public void removeAtRange(int, int);
+ method public void setValueAt(int, E!);
+ method public int size();
+ method public E! valueAt(int);
+ }
+
+}
+
diff --git a/collection/collection2/api/restricted_1.1.0-beta01.txt b/collection/collection2/api/restricted_1.1.0-beta01.txt
new file mode 100644
index 0000000..222ad00
--- /dev/null
+++ b/collection/collection2/api/restricted_1.1.0-beta01.txt
@@ -0,0 +1,183 @@
+// Signature format: 3.0
+package androidx.collection {
+
+ public class ArrayMap<K, V> extends androidx.collection.SimpleArrayMap<K,V> implements java.util.Map<K,V> {
+ ctor public ArrayMap();
+ ctor public ArrayMap(int);
+ ctor public ArrayMap(androidx.collection.SimpleArrayMap!);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public java.util.Set<java.util.Map.Entry<K!,V!>!>! entrySet();
+ method public java.util.Set<K!>! keySet();
+ method public void putAll(java.util.Map<? extends K,? extends V>!);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public java.util.Collection<V!>! values();
+ }
+
+ public final class ArraySet<E> implements java.util.Collection<E> java.util.Set<E> {
+ ctor public ArraySet();
+ ctor public ArraySet(int);
+ ctor public ArraySet(androidx.collection.ArraySet<E!>?);
+ ctor public ArraySet(java.util.Collection<E!>?);
+ method public boolean add(E?);
+ method public void addAll(androidx.collection.ArraySet<? extends E>);
+ method public boolean addAll(java.util.Collection<? extends E>);
+ method public void clear();
+ method public boolean contains(Object?);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public void ensureCapacity(int);
+ method public int indexOf(Object?);
+ method public boolean isEmpty();
+ method public java.util.Iterator<E!>! iterator();
+ method public boolean remove(Object?);
+ method public boolean removeAll(androidx.collection.ArraySet<? extends E>);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public E! removeAt(int);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public int size();
+ method public Object![] toArray();
+ method public <T> T![] toArray(T![]);
+ method public E? valueAt(int);
+ }
+
+ public final class CircularArray<E> {
+ ctor public CircularArray();
+ ctor public CircularArray(int);
+ method public void addFirst(E!);
+ method public void addLast(E!);
+ method public void clear();
+ method public E! get(int);
+ method public E! getFirst();
+ method public E! getLast();
+ method public boolean isEmpty();
+ method public E! popFirst();
+ method public E! popLast();
+ method public void removeFromEnd(int);
+ method public void removeFromStart(int);
+ method public int size();
+ }
+
+ public final class CircularIntArray {
+ ctor public CircularIntArray();
+ ctor public CircularIntArray(int);
+ method public void addFirst(int);
+ method public void addLast(int);
+ method public void clear();
+ method public int get(int);
+ method public int getFirst();
+ method public int getLast();
+ method public boolean isEmpty();
+ method public int popFirst();
+ method public int popLast();
+ method public void removeFromEnd(int);
+ method public void removeFromStart(int);
+ method public int size();
+ }
+
+ public class LongSparseArray<E> implements java.lang.Cloneable {
+ ctor public LongSparseArray();
+ ctor public LongSparseArray(int);
+ method public void append(long, E!);
+ method public void clear();
+ method public androidx.collection.LongSparseArray<E!>! clone();
+ method public boolean containsKey(long);
+ method public boolean containsValue(E!);
+ method @Deprecated public void delete(long);
+ method public E? get(long);
+ method public E! get(long, E!);
+ method public int indexOfKey(long);
+ method public int indexOfValue(E!);
+ method public boolean isEmpty();
+ method public long keyAt(int);
+ method public void put(long, E!);
+ method public void putAll(androidx.collection.LongSparseArray<? extends E>);
+ method public E? putIfAbsent(long, E!);
+ method public void remove(long);
+ method public boolean remove(long, Object!);
+ method public void removeAt(int);
+ method public E? replace(long, E!);
+ method public boolean replace(long, E!, E!);
+ method public void setValueAt(int, E!);
+ method public int size();
+ method public E! valueAt(int);
+ }
+
+ public class LruCache<K, V> {
+ ctor public LruCache(int);
+ method protected V? create(K);
+ method public final int createCount();
+ method protected void entryRemoved(boolean, K, V, V?);
+ method public final void evictAll();
+ method public final int evictionCount();
+ method public final V? get(K);
+ method public final int hitCount();
+ method public final int maxSize();
+ method public final int missCount();
+ method public final V? put(K, V);
+ method public final int putCount();
+ method public final V? remove(K);
+ method public void resize(int);
+ method public final int size();
+ method protected int sizeOf(K, V);
+ method public final java.util.Map<K!,V!>! snapshot();
+ method public final String toString();
+ method public void trimToSize(int);
+ }
+
+ public class SimpleArrayMap<K, V> {
+ ctor public SimpleArrayMap();
+ ctor public SimpleArrayMap(int);
+ ctor public SimpleArrayMap(androidx.collection.SimpleArrayMap<K!,V!>!);
+ method public void clear();
+ method public boolean containsKey(Object?);
+ method public boolean containsValue(Object!);
+ method public void ensureCapacity(int);
+ method public V? get(Object!);
+ method public V! getOrDefault(Object!, V!);
+ method public int indexOfKey(Object?);
+ method public boolean isEmpty();
+ method public K! keyAt(int);
+ method public V? put(K!, V!);
+ method public void putAll(androidx.collection.SimpleArrayMap<? extends K,? extends V>);
+ method public V? putIfAbsent(K!, V!);
+ method public V? remove(Object!);
+ method public boolean remove(Object!, Object!);
+ method public V! removeAt(int);
+ method public V? replace(K!, V!);
+ method public boolean replace(K!, V!, V!);
+ method public V! setValueAt(int, V!);
+ method public int size();
+ method public V! valueAt(int);
+ }
+
+ public class SparseArrayCompat<E> implements java.lang.Cloneable {
+ ctor public SparseArrayCompat();
+ ctor public SparseArrayCompat(int);
+ method public void append(int, E!);
+ method public void clear();
+ method public androidx.collection.SparseArrayCompat<E!>! clone();
+ method public boolean containsKey(int);
+ method public boolean containsValue(E!);
+ method @Deprecated public void delete(int);
+ method public E? get(int);
+ method public E! get(int, E!);
+ method public int indexOfKey(int);
+ method public int indexOfValue(E!);
+ method public boolean isEmpty();
+ method public int keyAt(int);
+ method public void put(int, E!);
+ method public void putAll(androidx.collection.SparseArrayCompat<? extends E>);
+ method public E? putIfAbsent(int, E!);
+ method public void remove(int);
+ method public boolean remove(int, Object!);
+ method public void removeAt(int);
+ method public void removeAtRange(int, int);
+ method public E? replace(int, E!);
+ method public boolean replace(int, E!, E!);
+ method public void setValueAt(int, E!);
+ method public int size();
+ method public E! valueAt(int);
+ }
+
+}
+
diff --git a/collection/collection2/api/restricted_1.1.0-beta02.txt b/collection/collection2/api/restricted_1.1.0-beta02.txt
new file mode 100644
index 0000000..da4f6cc
--- /dev/null
+++ b/collection/collection2/api/restricted_1.1.0-beta02.txt
@@ -0,0 +1 @@
+// Signature format: 3.0
diff --git a/collection/collection2/api/restricted_1.1.0-rc01.txt b/collection/collection2/api/restricted_1.1.0-rc01.txt
new file mode 100644
index 0000000..222ad00
--- /dev/null
+++ b/collection/collection2/api/restricted_1.1.0-rc01.txt
@@ -0,0 +1,183 @@
+// Signature format: 3.0
+package androidx.collection {
+
+ public class ArrayMap<K, V> extends androidx.collection.SimpleArrayMap<K,V> implements java.util.Map<K,V> {
+ ctor public ArrayMap();
+ ctor public ArrayMap(int);
+ ctor public ArrayMap(androidx.collection.SimpleArrayMap!);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public java.util.Set<java.util.Map.Entry<K!,V!>!>! entrySet();
+ method public java.util.Set<K!>! keySet();
+ method public void putAll(java.util.Map<? extends K,? extends V>!);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public java.util.Collection<V!>! values();
+ }
+
+ public final class ArraySet<E> implements java.util.Collection<E> java.util.Set<E> {
+ ctor public ArraySet();
+ ctor public ArraySet(int);
+ ctor public ArraySet(androidx.collection.ArraySet<E!>?);
+ ctor public ArraySet(java.util.Collection<E!>?);
+ method public boolean add(E?);
+ method public void addAll(androidx.collection.ArraySet<? extends E>);
+ method public boolean addAll(java.util.Collection<? extends E>);
+ method public void clear();
+ method public boolean contains(Object?);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public void ensureCapacity(int);
+ method public int indexOf(Object?);
+ method public boolean isEmpty();
+ method public java.util.Iterator<E!>! iterator();
+ method public boolean remove(Object?);
+ method public boolean removeAll(androidx.collection.ArraySet<? extends E>);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public E! removeAt(int);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public int size();
+ method public Object![] toArray();
+ method public <T> T![] toArray(T![]);
+ method public E? valueAt(int);
+ }
+
+ public final class CircularArray<E> {
+ ctor public CircularArray();
+ ctor public CircularArray(int);
+ method public void addFirst(E!);
+ method public void addLast(E!);
+ method public void clear();
+ method public E! get(int);
+ method public E! getFirst();
+ method public E! getLast();
+ method public boolean isEmpty();
+ method public E! popFirst();
+ method public E! popLast();
+ method public void removeFromEnd(int);
+ method public void removeFromStart(int);
+ method public int size();
+ }
+
+ public final class CircularIntArray {
+ ctor public CircularIntArray();
+ ctor public CircularIntArray(int);
+ method public void addFirst(int);
+ method public void addLast(int);
+ method public void clear();
+ method public int get(int);
+ method public int getFirst();
+ method public int getLast();
+ method public boolean isEmpty();
+ method public int popFirst();
+ method public int popLast();
+ method public void removeFromEnd(int);
+ method public void removeFromStart(int);
+ method public int size();
+ }
+
+ public class LongSparseArray<E> implements java.lang.Cloneable {
+ ctor public LongSparseArray();
+ ctor public LongSparseArray(int);
+ method public void append(long, E!);
+ method public void clear();
+ method public androidx.collection.LongSparseArray<E!>! clone();
+ method public boolean containsKey(long);
+ method public boolean containsValue(E!);
+ method @Deprecated public void delete(long);
+ method public E? get(long);
+ method public E! get(long, E!);
+ method public int indexOfKey(long);
+ method public int indexOfValue(E!);
+ method public boolean isEmpty();
+ method public long keyAt(int);
+ method public void put(long, E!);
+ method public void putAll(androidx.collection.LongSparseArray<? extends E>);
+ method public E? putIfAbsent(long, E!);
+ method public void remove(long);
+ method public boolean remove(long, Object!);
+ method public void removeAt(int);
+ method public E? replace(long, E!);
+ method public boolean replace(long, E!, E!);
+ method public void setValueAt(int, E!);
+ method public int size();
+ method public E! valueAt(int);
+ }
+
+ public class LruCache<K, V> {
+ ctor public LruCache(int);
+ method protected V? create(K);
+ method public final int createCount();
+ method protected void entryRemoved(boolean, K, V, V?);
+ method public final void evictAll();
+ method public final int evictionCount();
+ method public final V? get(K);
+ method public final int hitCount();
+ method public final int maxSize();
+ method public final int missCount();
+ method public final V? put(K, V);
+ method public final int putCount();
+ method public final V? remove(K);
+ method public void resize(int);
+ method public final int size();
+ method protected int sizeOf(K, V);
+ method public final java.util.Map<K!,V!>! snapshot();
+ method public final String toString();
+ method public void trimToSize(int);
+ }
+
+ public class SimpleArrayMap<K, V> {
+ ctor public SimpleArrayMap();
+ ctor public SimpleArrayMap(int);
+ ctor public SimpleArrayMap(androidx.collection.SimpleArrayMap<K!,V!>!);
+ method public void clear();
+ method public boolean containsKey(Object?);
+ method public boolean containsValue(Object!);
+ method public void ensureCapacity(int);
+ method public V? get(Object!);
+ method public V! getOrDefault(Object!, V!);
+ method public int indexOfKey(Object?);
+ method public boolean isEmpty();
+ method public K! keyAt(int);
+ method public V? put(K!, V!);
+ method public void putAll(androidx.collection.SimpleArrayMap<? extends K,? extends V>);
+ method public V? putIfAbsent(K!, V!);
+ method public V? remove(Object!);
+ method public boolean remove(Object!, Object!);
+ method public V! removeAt(int);
+ method public V? replace(K!, V!);
+ method public boolean replace(K!, V!, V!);
+ method public V! setValueAt(int, V!);
+ method public int size();
+ method public V! valueAt(int);
+ }
+
+ public class SparseArrayCompat<E> implements java.lang.Cloneable {
+ ctor public SparseArrayCompat();
+ ctor public SparseArrayCompat(int);
+ method public void append(int, E!);
+ method public void clear();
+ method public androidx.collection.SparseArrayCompat<E!>! clone();
+ method public boolean containsKey(int);
+ method public boolean containsValue(E!);
+ method @Deprecated public void delete(int);
+ method public E? get(int);
+ method public E! get(int, E!);
+ method public int indexOfKey(int);
+ method public int indexOfValue(E!);
+ method public boolean isEmpty();
+ method public int keyAt(int);
+ method public void put(int, E!);
+ method public void putAll(androidx.collection.SparseArrayCompat<? extends E>);
+ method public E? putIfAbsent(int, E!);
+ method public void remove(int);
+ method public boolean remove(int, Object!);
+ method public void removeAt(int);
+ method public void removeAtRange(int, int);
+ method public E? replace(int, E!);
+ method public boolean replace(int, E!, E!);
+ method public void setValueAt(int, E!);
+ method public int size();
+ method public E! valueAt(int);
+ }
+
+}
+
diff --git a/collection/collection2/api/restricted_current.ignore b/collection/collection2/api/restricted_current.ignore
new file mode 100644
index 0000000..805bf2c
--- /dev/null
+++ b/collection/collection2/api/restricted_current.ignore
@@ -0,0 +1,23 @@
+// Baseline format: 1.0
+ChangedSuperclass: androidx.collection.ArrayMap:
+ Class androidx.collection.ArrayMap superclass changed from androidx.collection.SimpleArrayMap to java.lang.Object
+
+
+RemovedClass: androidx.collection.ArraySet:
+ Removed class androidx.collection.ArraySet
+RemovedClass: androidx.collection.CircularArray:
+ Removed class androidx.collection.CircularArray
+RemovedClass: androidx.collection.CircularIntArray:
+ Removed class androidx.collection.CircularIntArray
+RemovedClass: androidx.collection.LongSparseArray:
+ Removed class androidx.collection.LongSparseArray
+RemovedClass: androidx.collection.LruCache:
+ Removed class androidx.collection.LruCache
+RemovedClass: androidx.collection.SimpleArrayMap:
+ Removed class androidx.collection.SimpleArrayMap
+RemovedClass: androidx.collection.SparseArrayCompat:
+ Removed class androidx.collection.SparseArrayCompat
+
+
+RemovedMethod: androidx.collection.ArrayMap#ArrayMap(androidx.collection.SimpleArrayMap):
+ Removed constructor androidx.collection.ArrayMap(androidx.collection.SimpleArrayMap)
diff --git a/collection/collection2/api/restricted_current.txt b/collection/collection2/api/restricted_current.txt
new file mode 100644
index 0000000..01eae79
--- /dev/null
+++ b/collection/collection2/api/restricted_current.txt
@@ -0,0 +1,22 @@
+// Signature format: 4.0
+package androidx.collection {
+
+ public class ArrayMap<K, V> implements java.util.Map<K,V> {
+ ctor public ArrayMap();
+ ctor public ArrayMap(int);
+ ctor public ArrayMap(SimpleArrayMap<K,V>);
+ method public boolean containsAll(java.util.Collection<?>);
+ method public boolean containsKey(Object?);
+ method public boolean containsValue(Object);
+ method public java.util.Set<java.util.Map.Entry<K!,V!>!> entrySet();
+ method public V? get(Object);
+ method public java.util.Set<K!> keySet();
+ method public void putAll(java.util.Map<? extends K,? extends V>);
+ method public V? remove(Object);
+ method public boolean removeAll(java.util.Collection<?>);
+ method public boolean retainAll(java.util.Collection<?>);
+ method public java.util.Collection<V!> values();
+ }
+
+}
+
diff --git a/collection/collection2/src/commonMain/kotlin/androidx/collection/ArraySet.kt b/collection/collection2/src/commonMain/kotlin/androidx/collection/ArraySet.kt
new file mode 100644
index 0000000..12e4f45
--- /dev/null
+++ b/collection/collection2/src/commonMain/kotlin/androidx/collection/ArraySet.kt
@@ -0,0 +1,371 @@
+/*
+ * 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.collection
+
+import kotlin.jvm.JvmOverloads
+
+class ArraySet<E>
+@JvmOverloads constructor(
+ capacity: Int = 0
+) : MutableCollection<E>, MutableSet<E> {
+ constructor(set: ArraySet<out E>?) : this(0) {
+ if (set != null) {
+ addAll(set)
+ }
+ }
+
+ constructor(set: Collection<E>?) : this(0) {
+ if (set != null) {
+ addAll(set)
+ }
+ }
+
+ private var hashes: IntArray
+ private var values: Array<E?>
+ init {
+ if (capacity == 0) {
+ hashes = EMPTY_INTS
+ @Suppress("UNCHECKED_CAST") // Empty array.
+ values = EMPTY_OBJECTS as Array<E?>
+ } else {
+ hashes = IntArray(capacity)
+ @Suppress("UNCHECKED_CAST") // We only get/set "E"s and nulls.
+ values = arrayOfNulls<Any>(capacity) as Array<E?>
+ }
+ }
+
+ private var _size: Int = 0
+
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open override val size: Int get() = _size
+
+ override fun isEmpty(): Boolean = _size == 0
+
+ /**
+ * @throws ConcurrentModificationException if the set has been concurrently modified.
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open fun ensureCapacity(minimumCapacity: Int) {
+ val oldSize = _size
+ if (hashes.size < minimumCapacity) {
+ hashes = hashes.copyOf(minimumCapacity)
+ values = values.copyOf(minimumCapacity)
+ }
+ if (_size != oldSize) {
+ throw ConcurrentModificationException()
+ }
+ }
+
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open fun indexOf(element: E): Int {
+ return if (element == null) {
+ indexOfNull()
+ } else {
+ indexOf(element, element.hashCode())
+ }
+ }
+
+ private fun binarySearch(hash: Int): Int {
+ try {
+ return hashes.binarySearch(size, hash)
+ } catch (_: IndexOutOfBoundsException) {
+ throw ConcurrentModificationException()
+ }
+ }
+
+ private fun indexOf(element: E, hash: Int) = indexOf(hash) { it == element }
+ private fun indexOfNull() = indexOf(0) { it == null }
+
+ private inline fun indexOf(hash: Int, predicate: (E?) -> Boolean): Int {
+ val size = _size
+ if (size == 0) {
+ // Important fast case: if nothing is in here, nothing to look for.
+ return 0.inv()
+ }
+
+ val index = binarySearch(hash)
+ if (index < 0) {
+ return index // Not found.
+ }
+
+ if (predicate(values[index])) {
+ return index // Found!
+ }
+
+ // Search matching hashes after the index.
+ var end = index + 1
+ while (end < size && hashes[end] == hash) {
+ if (predicate(values[end])) {
+ return end
+ }
+ end++
+ }
+
+ // Search matching hashes before the index.
+ for (i in index - 1 downTo 0) {
+ if (hashes[i] != hash) {
+ break
+ }
+ if (predicate(values[i])) {
+ return i
+ }
+ }
+
+ // Key not found -- return negative value indicating where a
+ // new entry for this key should go. We use the end of the
+ // hash chain to reduce the number of array entries that will
+ // need to be copied when inserting.
+ return end.inv()
+ }
+
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open fun valueAt(index: Int): E? = values[index]
+
+ /**
+ * @throws ConcurrentModificationException if the set has been concurrently modified.
+ */
+ override fun add(element: E): Boolean {
+ val oldSize = _size
+
+ val hash: Int
+ var index: Int
+ if (element == null) {
+ hash = 0
+ index = indexOfNull()
+ } else {
+ hash = element.hashCode()
+ index = indexOf(element, hash)
+ }
+
+ if (index >= 0) {
+ return false
+ }
+ index = index.inv()
+
+ // Grow the array if needed.
+ if (oldSize == hashes.size) {
+ val newSize = when {
+ oldSize >= BASE_SIZE_2X -> oldSize + (oldSize shr 1)
+ oldSize >= BASE_SIZE -> BASE_SIZE_2X
+ else -> BASE_SIZE
+ }
+ hashes = hashes.copyOf(newSize)
+ values = values.copyOf(newSize)
+ }
+
+ // Shift the array if needed.
+ if (index < oldSize) {
+ hashes.copyInto(
+ hashes,
+ destinationOffset = index + 1,
+ startIndex = index,
+ endIndex = oldSize
+ )
+ values.copyInto(
+ values,
+ destinationOffset = index + 1,
+ startIndex = index,
+ endIndex = oldSize
+ )
+ }
+
+ if (oldSize != _size || index >= hashes.size) {
+ throw ConcurrentModificationException()
+ }
+
+ hashes[index] = hash
+ values[index] = element
+
+ _size++
+ return true
+ }
+
+ override fun addAll(elements: Collection<E>): Boolean {
+ ensureCapacity(_size + elements.size)
+ var added = false
+ for (element in elements) {
+ added = added or add(element)
+ }
+ return added
+ }
+
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open fun addAll(elements: ArraySet<E>) {
+ val arraySize = elements._size
+ if (_size == 0) {
+ if (arraySize > 0) {
+ hashes = elements.hashes.copyOf()
+ values = elements.values.copyOf()
+ _size = arraySize
+ }
+ } else {
+ for (i in 0 until arraySize) {
+ add(elements.valueAt(i)!!)
+ }
+ }
+ }
+
+ override fun remove(element: E): Boolean {
+ val index = indexOf(element)
+ if (index >= 0) {
+ removeAt(index)
+ return true
+ }
+ return false
+ }
+
+ /**
+ * @throws ConcurrentModificationException if the set has been concurrently modified.
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open fun removeAt(index: Int): E? {
+ val oldSize = _size
+ val value = values[index]
+
+ // TODO assert oldSize > 0, safe because we would have IOOBE'd above
+ if (oldSize == 1) {
+ clear()
+ } else {
+ val newSize = oldSize - 1
+
+ val oldHashes = hashes
+ val oldValues = values
+ val oldStorageSize = oldHashes.size
+ if (oldStorageSize > BASE_SIZE_2X && oldSize < oldStorageSize / 3) {
+
+ // Shrinking enough to reduce size of arrays. We don't allow it to
+ // shrink smaller than BASE_SIZE_2X to avoid flapping between that and BASE_SIZE.
+ val newStorageSize = when {
+ oldSize > BASE_SIZE_2X -> oldSize + (oldSize shr 1)
+ else -> BASE_SIZE_2X
+ }
+ val newHashes = IntArray(newStorageSize)
+ @Suppress("UNCHECKED_CAST") // We only get/set "E"s and nulls.
+ val newValues = arrayOfNulls<Any?>(newStorageSize) as Array<E?>
+
+ if (index > 0) {
+ oldHashes.copyInto(newHashes, endIndex = index)
+ oldValues.copyInto(newValues, endIndex = index)
+ }
+ if (index < newSize) {
+ oldHashes.copyInto(
+ newHashes,
+ destinationOffset = index,
+ startIndex = index + 1,
+ endIndex = oldSize
+ )
+ oldValues.copyInto(
+ newValues,
+ destinationOffset = index,
+ startIndex = index + 1,
+ endIndex = oldSize
+ )
+ }
+
+ hashes = newHashes
+ values = newValues
+ } else {
+ if (index < newSize) {
+ oldHashes.copyInto(
+ oldHashes,
+ destinationOffset = index,
+ startIndex = index + 1,
+ endIndex = oldSize
+ )
+ oldValues.copyInto(
+ oldValues,
+ destinationOffset = index,
+ startIndex = index + 1,
+ endIndex = oldSize
+ )
+ }
+ oldValues[newSize] = null
+ }
+
+ if (oldSize != _size) {
+ throw ConcurrentModificationException()
+ }
+ _size = newSize
+ }
+ return value
+ }
+
+ override fun removeAll(elements: Collection<E>): Boolean {
+ var removed = false
+ for (element in elements) {
+ removed = removed or remove(element)
+ }
+ return removed
+ }
+
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open fun removeAll(elements: ArraySet<out E>): Boolean {
+ // TODO: If array is sufficiently large, a marking approach might be beneficial. In a first
+ // pass, use the property that the sets are sorted by hash to make this linear passes
+ // (except for hash collisions, which means worst case still n*m), then do one collection
+ // pass into a new array. This avoids binary searches and excessive memcpy.
+
+ val elementsSize = elements._size
+ val originalSize = _size
+ for (i in 0 until elementsSize) {
+ remove(elements.valueAt(i)!!) // TODO unsafeCast?
+ }
+ return originalSize != _size
+ }
+
+ override fun clear() {
+ hashes = EMPTY_INTS
+ @Suppress("UNCHECKED_CAST") // Empty array.
+ values = EMPTY_OBJECTS as Array<E?>
+ _size = 0
+ }
+
+ override fun retainAll(elements: Collection<E>): Boolean {
+ var removed = false
+ for (i in 0 until _size) {
+ if (values[i] !in elements) {
+ removeAt(i)
+ removed = true
+ }
+ }
+ return removed
+ }
+
+ override fun contains(element: E): Boolean = indexOf(element) >= 0
+
+ override fun containsAll(elements: Collection<E>): Boolean {
+ return elements.all { it in this }
+ }
+
+ override fun iterator(): MutableIterator<E> {
+ return Iterator(_size)
+ }
+
+ private inner class Iterator(size: Int) : IndexBasedMutableIterator<E>(size) {
+ @Suppress("UNCHECKED_CAST") // Assume base iterator only accessing valid indices.
+ override fun get(index: Int) = values[index] as E
+
+ override fun remove(index: Int) {
+ removeAt(index)
+ }
+ }
+
+ private companion object {
+ private const val BASE_SIZE = 4
+ private const val BASE_SIZE_2X = BASE_SIZE * 2
+ }
+}
diff --git a/collection/collection2/src/commonMain/kotlin/androidx/collection/CircularArray.kt b/collection/collection2/src/commonMain/kotlin/androidx/collection/CircularArray.kt
new file mode 100644
index 0000000..63d9f48
--- /dev/null
+++ b/collection/collection2/src/commonMain/kotlin/androidx/collection/CircularArray.kt
@@ -0,0 +1,271 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.collection
+
+import kotlin.js.JsName
+import kotlin.jvm.JvmName
+import kotlin.jvm.JvmOverloads
+
+/**
+ * CircularArray is a generic circular array data structure that provides O(1) random read, O(1)
+ * prepend and O(1) append. The CircularArray automatically grows its capacity when number of added
+ * items is over its capacity.
+ */
+class CircularArray<E>
+/**
+ * Creates a circular array with capacity for at least `minCapacity`
+ * elements.
+ *
+ * @param minCapacity the minimum capacity, between 1 and 2^30 inclusive
+ */
+@JvmOverloads
+constructor(minCapacity: Int = 8) {
+ private var elements: Array<E?>
+ private var head = 0
+ private var tail = 0
+ private var capacityBitmask: Int
+
+ init {
+ require(minCapacity >= 1) { "capacity must be >= 1" }
+ require(minCapacity <= 2 shl 29) { "capacity must be <= 2^30" }
+
+ // If minCapacity isn't a power of 2, round up to the next highest power of 2.
+ val arrayCapacity = if (minCapacity.countOneBits() != 1) {
+ (minCapacity - 1).takeHighestOneBit() shl 1
+ } else {
+ minCapacity
+ }
+
+ capacityBitmask = arrayCapacity - 1
+ @Suppress("UNCHECKED_CAST") // We only get/set "E"s and nulls.
+ elements = arrayOfNulls<Any>(arrayCapacity) as Array<E?>
+ }
+
+ /**
+ * Get first element of the CircularArray.
+ *
+ * @return The first element.
+ * @throws [IndexOutOfBoundsException] if CircularArray is empty.
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open val first: E
+ get() {
+ if (head == tail) throw indexOutOfBounds()
+ @Suppress("UNCHECKED_CAST") // Guarded by above non-empty conditional.
+ return elements[head] as E
+ }
+
+ /**
+ * Get last element of the CircularArray.
+ *
+ * @return The last element.
+ * @throws [IndexOutOfBoundsException] if CircularArray is empty.
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open val last: E
+ get() {
+ if (head == tail) throw indexOutOfBounds()
+ @Suppress("UNCHECKED_CAST") // Guarded by above non-empty conditional.
+ return elements[tail - 1 and capacityBitmask] as E
+ }
+
+ /** Return true if `size()` is 0. */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open fun isEmpty(): Boolean = head == tail
+
+ private fun doubleCapacity() {
+ val n = elements.size
+ val r = n - head
+ val newCapacity = n shl 1
+ if (newCapacity < 0) {
+ throw RuntimeException("Max array capacity exceeded")
+ }
+ @Suppress("UNCHECKED_CAST") // We only get/set "E"s and nulls.
+ val a = arrayOfNulls<Any>(newCapacity) as Array<E?>
+ elements.copyInto(a, startIndex = head, endIndex = n)
+ elements.copyInto(a, destinationOffset = r, startIndex = 0, endIndex = head)
+ elements = a
+ head = 0
+ tail = n
+ capacityBitmask = newCapacity - 1
+ }
+
+ /**
+ * Add an element in front of the CircularArray.
+ *
+ * @param e Element to add.
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ @JsName("addFirst")
+ open fun addFirst(e: E) {
+ head = head - 1 and capacityBitmask
+ elements[head] = e
+ if (head == tail) {
+ doubleCapacity()
+ }
+ }
+
+ /**
+ * Add an element at end of the CircularArray.
+ *
+ * @param e Element to add.
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ @JsName("addLast")
+ open fun addLast(e: E) {
+ elements[tail] = e
+ tail = tail + 1 and capacityBitmask
+ if (tail == head) {
+ doubleCapacity()
+ }
+ }
+
+ /**
+ * Remove first element from front of the CircularArray and return it.
+ *
+ * @return The element removed.
+ * @throws IndexOutOfBoundsException if CircularArray is empty.
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open fun popFirst(): E {
+ if (head == tail) {
+ throw indexOutOfBounds()
+ }
+ @Suppress("UNCHECKED_CAST") // Guarded by above non-empty conditional.
+ val result = elements[head] as E
+ elements[head] = null
+ head = head + 1 and capacityBitmask
+ return result
+ }
+
+ /**
+ * Remove last element from end of the CircularArray and return it.
+ *
+ * @return The element removed.
+ * @throws IndexOutOfBoundsException if CircularArray is empty.
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open fun popLast(): E {
+ if (head == tail) {
+ throw indexOutOfBounds()
+ }
+ val t = tail - 1 and capacityBitmask
+ @Suppress("UNCHECKED_CAST") // Guarded by above non-empty conditional.
+ val result = elements[t] as E
+ elements[t] = null
+ tail = t
+ return result
+ }
+
+ /** Remove all elements from the CircularArray. */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open fun clear() {
+ removeFromStart(size)
+ }
+
+ /**
+ * Remove multiple elements from front of the CircularArray, ignored when [numOfElements]
+ * is less than or equal to 0.
+ *
+ * @param numOfElements Number of elements to remove.
+ * @throws IndexOutOfBoundsException if [numOfElements] is larger than [size]
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open fun removeFromStart(numOfElements: Int) {
+ if (numOfElements <= 0) {
+ return
+ }
+ if (numOfElements > size) {
+ throw indexOutOfBounds()
+ }
+ var end = elements.size
+ if (numOfElements < end - head) {
+ end = head + numOfElements
+ }
+ for (i in head until end) {
+ elements[i] = null
+ }
+ val removed = end - head
+ head = head + removed and capacityBitmask
+ val remaining = numOfElements - removed
+ if (remaining > 0) {
+ // head wrapped to 0
+ for (i in 0 until remaining) {
+ elements[i] = null
+ }
+ head = remaining
+ }
+ }
+
+ /**
+ * Remove multiple elements from end of the CircularArray, ignored when [numOfElements]
+ * is less than or equal to 0.
+ *
+ * @param numOfElements Number of elements to remove.
+ * @throws IndexOutOfBoundsException if [numOfElements] is larger than [size]
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open fun removeFromEnd(numOfElements: Int) {
+ if (numOfElements <= 0) {
+ return
+ }
+ if (numOfElements > size) {
+ throw indexOutOfBounds()
+ }
+ var start = 0
+ if (numOfElements < tail) {
+ start = tail - numOfElements
+ }
+ for (i in start until tail) {
+ elements[i] = null
+ }
+ val removed = tail - start
+ tail -= removed
+ val remaining = numOfElements - removed
+ if (remaining > 0) {
+ // tail wrapped to elements.length
+ tail = elements.size
+ val newTail = tail - remaining
+ for (i in newTail until tail) {
+ elements[i] = null
+ }
+ tail = newTail
+ }
+ }
+
+ /**
+ * Get nth (0 <= n <= size()-1) element of the CircularArray.
+ *
+ * @param n The zero based element index in the CircularArray.
+ * @return The nth element.
+ * @throws [IndexOutOfBoundsException] if n < 0 or n >= size().
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open operator fun get(n: Int): E {
+ if (n < 0 || n >= size) {
+ throw indexOutOfBounds()
+ }
+ @Suppress("UNCHECKED_CAST") // Guarded by above valid range conditional.
+ return elements[head + n and capacityBitmask] as E
+ }
+
+ /** Get number of elements in the CircularArray. */
+ @get:JvmName("size") // For binary compatibility.
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open val size: Int get() {
+ return tail - head and capacityBitmask
+ }
+}
diff --git a/collection/collection2/src/commonMain/kotlin/androidx/collection/CircularIntArray.kt b/collection/collection2/src/commonMain/kotlin/androidx/collection/CircularIntArray.kt
new file mode 100644
index 0000000..d75c789
--- /dev/null
+++ b/collection/collection2/src/commonMain/kotlin/androidx/collection/CircularIntArray.kt
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.collection
+
+import kotlin.jvm.JvmName
+import kotlin.jvm.JvmOverloads
+
+/**
+ * CircularIntArray is a circular integer array data structure that provides O(1) random read, O(1)
+ * prepend and O(1) append. The CircularIntArray automatically grows its capacity when number of
+ * added integers is over its capacity.
+ */
+class CircularIntArray
+/**
+ * Creates a circular array with capacity for at least `minCapacity`
+ * elements.
+ *
+ * @param minCapacity the minimum capacity, between 1 and 2^30 inclusive
+ */
+@JvmOverloads constructor(minCapacity: Int = 8) {
+ private var elements: IntArray
+ private var head = 0
+ private var tail = 0
+ private var capacityBitmask: Int
+
+ init {
+ require(minCapacity >= 1) { "capacity must be >= 1" }
+ require(minCapacity <= 2 shl 29) { "capacity must be <= 2^30" }
+
+ // If minCapacity isn't a power of 2, round up to the next highest
+ // power of 2.
+ val arrayCapacity = if (minCapacity.countOneBits() != 1) {
+ (minCapacity - 1).takeHighestOneBit() shl 1
+ } else {
+ minCapacity
+ }
+
+ capacityBitmask = arrayCapacity - 1
+ elements = IntArray(arrayCapacity)
+ }
+
+ /**
+ * Get first integer of the CircularIntArray.
+ *
+ * @return The first integer.
+ * @throws [IndexOutOfBoundsException] if CircularIntArray is empty.
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open val first: Int
+ get() {
+ if (head == tail) throw indexOutOfBounds()
+ return elements[head]
+ }
+
+ /**
+ * Get last integer of the CircularIntArray.
+ *
+ * @return The last integer.
+ * @throws [IndexOutOfBoundsException] if CircularIntArray is empty.
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open val last: Int
+ get() {
+ if (head == tail) throw indexOutOfBounds()
+ return elements[tail - 1 and capacityBitmask]
+ }
+
+ /** Return true if `size()` is 0. */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open fun isEmpty(): Boolean = head == tail
+
+ private fun doubleCapacity() {
+ val n = elements.size
+ val r = n - head
+ val newCapacity = n shl 1
+ if (newCapacity < 0) {
+ throw RuntimeException("Max array capacity exceeded")
+ }
+ val a = IntArray(newCapacity)
+ elements.copyInto(a, startIndex = head, endIndex = n)
+ elements.copyInto(a, destinationOffset = r, startIndex = 0, endIndex = head)
+ elements = a
+ head = 0
+ tail = n
+ capacityBitmask = newCapacity - 1
+ }
+
+ /**
+ * Add an integer in front of the CircularIntArray.
+ *
+ * @param e Integer to add.
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open fun addFirst(e: Int) {
+ head = head - 1 and capacityBitmask
+ elements[head] = e
+ if (head == tail) {
+ doubleCapacity()
+ }
+ }
+
+ /**
+ * Add an integer at end of the CircularIntArray.
+ *
+ * @param e Integer to add.
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open fun addLast(e: Int) {
+ elements[tail] = e
+ tail = tail + 1 and capacityBitmask
+ if (tail == head) {
+ doubleCapacity()
+ }
+ }
+
+ /**
+ * Remove first integer from front of the CircularIntArray and return it.
+ *
+ * @return The integer removed.
+ * @throws IndexOutOfBoundsException if CircularIntArray is empty.
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open fun popFirst(): Int {
+ if (head == tail) throw indexOutOfBounds()
+ val result = elements[head]
+ head = head + 1 and capacityBitmask
+ return result
+ }
+
+ /**
+ * Remove last integer from end of the CircularIntArray and return it.
+ * @return The integer removed.
+ * @throws IndexOutOfBoundsException if CircularIntArray is empty.
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open fun popLast(): Int {
+ if (head == tail) throw indexOutOfBounds()
+ val t = tail - 1 and capacityBitmask
+ val result = elements[t]
+ tail = t
+ return result
+ }
+
+ /**
+ * Remove all integers from the CircularIntArray.
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open fun clear() {
+ tail = head
+ }
+
+ /**
+ * Remove multiple integers from front of the CircularIntArray, ignore when numOfElements
+ * is less than or equals to 0.
+ * @param numOfElements Number of integers to remove.
+ * @throws IndexOutOfBoundsException if numOfElements is larger than
+ * [.size]
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open fun removeFromStart(numOfElements: Int) {
+ if (numOfElements <= 0) {
+ return
+ }
+ if (numOfElements > size) {
+ throw indexOutOfBounds()
+ }
+ head = head + numOfElements and capacityBitmask
+ }
+
+ /**
+ * Remove multiple elements from end of the CircularIntArray, ignore when numOfElements
+ * is less than or equals to 0.
+ * @param numOfElements Number of integers to remove.
+ * @throws IndexOutOfBoundsException if numOfElements is larger than [size]
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open fun removeFromEnd(numOfElements: Int) {
+ if (numOfElements <= 0) {
+ return
+ }
+ if (numOfElements > size) {
+ throw indexOutOfBounds()
+ }
+ tail = tail - numOfElements and capacityBitmask
+ }
+
+ /**
+ * Get nth (0 <= n <= size()-1) integer of the CircularIntArray.
+ * @param n The zero based element index in the CircularIntArray.
+ * @return The nth integer.
+ * @throws [IndexOutOfBoundsException] if n < 0 or n >= size().
+ */
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open operator fun get(n: Int): Int {
+ if (n < 0 || n >= size) throw indexOutOfBounds()
+ return elements[head + n and capacityBitmask]
+ }
+
+ /** Get number of integers in the CircularIntArray. */
+ @get:JvmName("size") // For binary compatibility.
+ @Suppress("NON_FINAL_MEMBER_IN_FINAL_CLASS") // For japicmp.
+ open val size: Int get() {
+ return tail - head and capacityBitmask
+ }
+}
diff --git a/collection/collection2/src/commonMain/kotlin/androidx/collection/LongSparseArray.kt b/collection/collection2/src/commonMain/kotlin/androidx/collection/LongSparseArray.kt
new file mode 100644
index 0000000..096f586
--- /dev/null
+++ b/collection/collection2/src/commonMain/kotlin/androidx/collection/LongSparseArray.kt
@@ -0,0 +1,322 @@
+/*
+ * 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.
+ */
+
+/** Avoid conflict (and R8 dup. classes failures) with collection-ktx. */
+// TODO: Remove this after collection-ktx is merged
+@file:JvmName("LongSparseArray_Ext")
+@file:Suppress("NOTHING_TO_INLINE") // Avoiding additional invocation indirection.
+
+package androidx.collection
+
+import kotlin.jvm.JvmName
+
+expect class LongSparseArray<E>(initialCapacity: Int = 10) {
+ internal var keys: LongArray
+ internal var values: Array<Any?>
+ internal var garbage: Boolean
+
+ @Suppress("PropertyName") // Normal backing field name but internal for common code.
+ internal var _size: Int
+
+ constructor(array: LongSparseArray<E>)
+
+ val size: Int
+ fun isEmpty(): Boolean
+
+ operator fun get(key: Long): E?
+ fun get(key: Long, default: E): E
+
+ fun put(key: Long, value: E) // TODO operator
+ fun putAll(other: LongSparseArray<out E>)
+ fun putIfAbsent(key: Long, value: E): E?
+ fun append(key: Long, value: E)
+
+ fun keyAt(index: Int): Long
+
+ fun valueAt(index: Int): E
+ fun setValueAt(index: Int, value: E)
+
+ fun indexOfKey(key: Long): Int
+ fun indexOfValue(value: E): Int
+
+ fun containsKey(key: Long): Boolean
+ fun containsValue(value: E): Boolean
+
+ fun clear()
+
+ fun remove(key: Long)
+ fun remove(key: Long, value: Any?): Boolean
+ fun removeAt(index: Int)
+
+ fun replace(key: Long, value: E): E?
+ fun replace(key: Long, oldValue: E?, newValue: E): Boolean
+}
+
+internal inline fun <E> LongSparseArray<E>.commonSize(): Int {
+ if (garbage) {
+ gc()
+ }
+ return _size
+}
+
+internal inline fun <E> LongSparseArray<E>.commonIsEmpty(): Boolean {
+ return size == 0
+}
+
+internal inline fun <T, E : T> LongSparseArray<E>.commonGet(key: Long, default: T): T {
+ val i = keys.binarySearch(_size, key)
+ if (i >= 0) {
+ val value = values[i]
+ if (value !== DELETED) {
+ @Suppress("UNCHECKED_CAST") // Guaranteed by positive index and DELETED check.
+ return value as E
+ }
+ }
+ return default
+}
+
+internal inline fun <E> LongSparseArray<E>.commonPut(key: Long, value: E) {
+ var index = keys.binarySearch(_size, key)
+ if (index >= 0) {
+ values[index] = value
+ } else {
+ index = index.inv()
+ if (index < _size && values[index] === DELETED) {
+ keys[index] = key
+ values[index] = value
+ return
+ }
+ if (garbage && _size >= keys.size) {
+ gc()
+ // Search again because indices may have changed.
+ index = keys.binarySearch(_size, key).inv()
+ }
+ if (_size >= keys.size) {
+ val newSize = idealIntArraySize(_size + 1)
+ keys = keys.copyOf(newSize)
+ values = values.copyOf(newSize)
+ }
+ if (_size - index != 0) {
+ keys.copyInto(keys, destinationOffset = index + 1, startIndex = index, endIndex = _size)
+ values.copyInto(
+ values,
+ destinationOffset = index + 1,
+ startIndex = index,
+ endIndex = _size
+ )
+ }
+ keys[index] = key
+ values[index] = value
+ _size++
+ }
+}
+
+internal inline fun <E> LongSparseArray<E>.commonPutAll(other: LongSparseArray<out E>) {
+ for (i in 0 until other.size) {
+ @Suppress("UNCHECKED_CAST") // Guaranteed by valid index.
+ put(other.keys[i], other.values[i] as E)
+ }
+}
+
+internal inline fun <E> LongSparseArray<E>.commonPutIfAbsent(key: Long, value: E): E? {
+ val mapValue = get(key)
+ if (mapValue == null) {
+ // TODO avoid double binary search here
+ put(key, value)
+ }
+ return mapValue
+}
+
+internal inline fun <E> LongSparseArray<E>.commonAppend(key: Long, value: E) {
+ if (_size != 0 && key <= keys[_size - 1]) {
+ put(key, value)
+ return
+ }
+ if (garbage && _size >= keys.size) {
+ gc()
+ }
+ val pos = _size
+ if (pos >= keys.size) {
+ val newSize = idealIntArraySize(pos + 1)
+ keys = keys.copyOf(newSize)
+ values = values.copyOf(newSize)
+ }
+ keys[pos] = key
+ values[pos] = value
+ _size = pos + 1
+}
+
+internal inline fun <E> LongSparseArray<E>.commonKeyAt(index: Int): Long {
+ if (garbage) {
+ gc()
+ }
+ return keys[index]
+}
+
+internal inline fun <E> LongSparseArray<E>.commonValueAt(index: Int): E {
+ if (garbage) {
+ gc()
+ }
+ @Suppress("UNCHECKED_CAST") // Guaranteed by having run GC.
+ return values[index] as E
+}
+
+internal inline fun <E> LongSparseArray<E>.commonSetValueAt(index: Int, value: E) {
+ if (garbage) {
+ gc()
+ }
+ values[index] = value
+}
+
+internal inline fun <E> LongSparseArray<E>.commonIndexOfKey(key: Long): Int {
+ if (garbage) {
+ gc()
+ }
+ return keys.binarySearch(_size, key)
+}
+
+internal inline fun <E> LongSparseArray<E>.commonIndexOfValue(value: E): Int {
+ if (garbage) {
+ gc()
+ }
+ for (i in 0 until _size) {
+ if (values[i] === value) {
+ return i
+ }
+ }
+ return -1
+}
+
+internal inline fun <E> LongSparseArray<E>.commonContainsKey(key: Long): Boolean {
+ return indexOfKey(key) >= 0
+}
+
+internal inline fun <E> LongSparseArray<E>.commonContainsValue(value: E): Boolean {
+ return indexOfValue(value) >= 0
+}
+
+internal inline fun <E> LongSparseArray<E>.commonClear() {
+ values.fill(null, toIndex = _size)
+ _size = 0
+ garbage = false
+}
+
+internal inline fun <E> LongSparseArray<E>.commonRemove(key: Long) {
+ val index = keys.binarySearch(_size, key)
+ if (index >= 0 && values[index] !== DELETED) {
+ values[index] = DELETED
+ garbage = true
+ }
+}
+
+internal inline fun <E> LongSparseArray<E>.commonRemove(key: Long, value: Any?): Boolean {
+ val index = indexOfKey(key)
+ if (index >= 0) {
+ val mapValue = valueAt(index)
+ if (value == mapValue) {
+ removeAt(index)
+ return true
+ }
+ }
+ return false
+}
+
+internal inline fun <E> LongSparseArray<E>.commonRemoveAt(index: Int) {
+ if (values[index] !== DELETED) {
+ values[index] = DELETED
+ garbage = true
+ }
+}
+
+internal inline fun <E> LongSparseArray<E>.commonReplace(key: Long, value: E): E? {
+ val index = indexOfKey(key)
+ if (index >= 0) {
+ @Suppress("UNCHECKED_CAST") // Guaranteed by index which would have run GC.
+ val oldValue = values[index] as E
+ values[index] = value
+ return oldValue
+ }
+ return null
+}
+
+internal inline fun <E> LongSparseArray<E>.commonReplace(
+ key: Long,
+ oldValue: E?,
+ newValue: E
+): Boolean {
+ val index = indexOfKey(key)
+ if (index >= 0) {
+ val mapValue = values[index]
+ if (mapValue == oldValue) {
+ values[index] = newValue
+ return true
+ }
+ }
+ return false
+}
+
+internal inline fun <E> LongSparseArray<E>.commonClone(): LongSparseArray<E> {
+ val new = LongSparseArray<E>(0)
+ new._size = _size
+ new.keys = keys.copyOf()
+ new.values = values.copyOf()
+ new.garbage = garbage
+ new.gc()
+ return new
+}
+
+internal inline fun <E> LongSparseArray<E>.commonToString(): String {
+ if (size == 0) {
+ return "{}"
+ }
+ return buildString(_size * 20) {
+ append('{')
+ for (i in 0 until _size) {
+ if (i > 0) {
+ append(", ")
+ }
+ val key = keyAt(i)
+ append(key)
+ append('=')
+ val value = valueAt(i)
+ if (value !== this) {
+ append(value)
+ } else {
+ append("(this Map)")
+ }
+ }
+ append('}')
+ }
+}
+
+internal fun <E> LongSparseArray<E>.gc() {
+ var newSize = 0
+ val keys = keys
+ val values = values
+ for (i in 0 until _size) {
+ val value = values[i]
+ if (value !== DELETED) {
+ if (i != newSize) {
+ keys[newSize] = keys[i]
+ values[newSize] = value
+ values[i] = null
+ }
+ newSize++
+ }
+ }
+ garbage = false
+ _size = newSize
+}
diff --git a/collection/collection2/src/commonMain/kotlin/androidx/collection/LruCache.kt b/collection/collection2/src/commonMain/kotlin/androidx/collection/LruCache.kt
new file mode 100644
index 0000000..36f9739
--- /dev/null
+++ b/collection/collection2/src/commonMain/kotlin/androidx/collection/LruCache.kt
@@ -0,0 +1,357 @@
+/*
+ * 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.collection
+
+import kotlin.js.JsName
+import kotlin.jvm.JvmName
+
+/**
+ * Static library version of {@code android.util.LruCache}. Used to write apps
+ * that run on API levels prior to 12. When running on API level 12 or above,
+ * this implementation is still used; it does not try to switch to the
+ * framework's implementation. See the framework SDK documentation for a class
+ * overview.
+ */
+open class LruCache<K, V> {
+
+ private val monitor = createSynchronizedObject()
+ private val map: HashMap<K, V>
+ private val keySet: LinkedHashSet<K>
+
+ /** Size of this cache in units. Not necessarily the number of elements. */
+ @get:JvmName("size")
+ public var size: Int = 0
+ /**
+ * For caches that do not override {@link #sizeOf}, this returns the number
+ * of entries in the cache. For all other caches, this returns the sum of
+ * the sizes of the entries in this cache.
+ */
+ get() = synchronizedValue { return field }
+ private set
+
+ private var maxSize = 0
+
+ private var putCount = 0
+ private var createCount = 0
+ private var evictionCount = 0
+ private var hitCount = 0
+ private var missCount = 0
+
+ /**
+ * @param maxSize for caches that do not override {@link #sizeOf}, this is
+ * the maximum number of entries in the cache. For all other caches,
+ * this is the maximum sum of the sizes of the entries in this cache.
+ */
+ @JsName("LruCache\$int")
+ constructor(maxSize: Int) {
+ require(maxSize > 0) { "maxSize <= 0" }
+ this.maxSize = maxSize
+ map = HashMap<K, V>(0, 0.75f)
+ keySet = LinkedHashSet<K>()
+ }
+
+ /**
+ * Sets the size of the cache.
+ *
+ * @param maxSize The new maximum size.
+ */
+ open fun resize(maxSize: Int) {
+ require(maxSize > 0) { "maxSize <= 0" }
+
+ synchronizedOperation(monitor) {
+ this.maxSize = maxSize
+ }
+ trimToSize(maxSize)
+ }
+
+ /**
+ * Returns the value for {@code key} if it exists in the cache or can be
+ * created by {@code #create}. If a value was returned, it is moved to the
+ * head of the queue. This returns null if a value is not cached and cannot
+ * be created.
+ */
+ @JsName("get")
+ fun get(key: K): V? {
+ var mapValue: V? = null
+
+ synchronizedOperation(monitor) {
+ mapValue = map.get(key)
+ if (mapValue != null) {
+ // Push the key to the end of the keySet as the cached entry gets hit.
+ keySet.remove(key)
+ keySet.add(key)
+ hitCount++
+ return mapValue
+ } else {
+ missCount++
+ }
+ }
+
+ val createdValue: V? = create(key)
+ if (createdValue == null) {
+ return null
+ }
+
+ synchronizedOperation(monitor) {
+ createCount++
+ val previousValue: V? = map.put(key, createdValue)
+ // Push the key to the end of the keySet as the cached entry gets hit.
+ keySet.remove(key)
+ keySet.add(key)
+ if (previousValue != null) {
+ // There was a conflict so undo that last put
+ map.put(key, previousValue)
+ mapValue = previousValue
+ } else {
+ size += safeSizeOf(key, createdValue)
+ }
+ }
+
+ if (mapValue != null) {
+ entryRemoved(false, key, createdValue, mapValue)
+ return mapValue
+ } else {
+ trimToSize(maxSize)
+ return createdValue
+ }
+ }
+
+ /**
+ * Caches [value] for [key]. The value is moved to the head of
+ * the queue.
+ *
+ * @return the previous value mapped by [key].
+ * @throws NullPointerException if [key] or [value] is null
+ */
+ @JsName("put")
+ fun put(key: K, value: V): V? {
+ // Must throw NPE for JVM interop contract.
+ if (key == null || value == null) {
+ throw NullPointerException()
+ }
+
+ var previous: V? = null
+ synchronizedOperation(monitor) {
+ putCount++
+ size += safeSizeOf(key, value)
+ previous = map.put(key, value)
+ if (previous != null) {
+ size -= safeSizeOf(key, previous!!)
+ }
+ if (keySet.contains(key)) {
+ keySet.remove(key)
+ }
+ keySet.add(key)
+ }
+
+ if (previous != null) {
+ entryRemoved(false, key, previous!!, value)
+ }
+
+ trimToSize(maxSize)
+ return previous
+ }
+
+ /**
+ * Remove the eldest entries until the total of remaining entries is at or
+ * below the requested size.
+ *
+ * @param maxSize the maximum size of the cache before returning. May be -1
+ * to evict even 0-sized elements.
+ * @throws IllegalStateException
+ */
+ open fun trimToSize(maxSize: Int) {
+ while (true) {
+ var key: K? = null
+ var value: V? = null
+
+ synchronizedOperation(monitor) {
+ if (size < 0 ||
+ (map.isEmpty() && size != 0) ||
+ (map.isEmpty() != keySet.isEmpty())
+ ) {
+ throw IllegalStateException("map/keySet size inconsistency")
+ }
+
+ if (size > maxSize && !map.isEmpty()) {
+ key = keySet.first()
+ value = map.get(key) ?: throw IllegalStateException(
+ "inconsistent " +
+ "state"
+ )
+ map.remove(key)
+ keySet.remove(key)
+ size -= safeSizeOf(key!!, value!!)
+ evictionCount++
+ }
+ }
+
+ if (key == null && value == null) {
+ break
+ } else {
+ entryRemoved(true, key!!, value!!, null)
+ }
+ }
+ }
+
+ /**
+ * Removes the entry for [key] if it exists.
+ *
+ * @return the previous value mapped by [key].
+ * @throws NullPointerException if [key] is null from a JVM caller.
+ */
+ fun remove(key: K): V? {
+ // Must throw NPE for JVM interop contract.
+ if (key == null) {
+ throw NullPointerException()
+ }
+
+ var previous: V? = null
+ synchronizedOperation(monitor) {
+ previous = map.remove(key)
+ keySet.remove(key)
+ if (previous != null) {
+ size -= safeSizeOf(key, previous!!)
+ }
+ }
+
+ if (previous != null) {
+ entryRemoved(false, key, previous!!, null)
+ }
+
+ return previous
+ }
+
+ /**
+ * Called for entries that have been evicted or removed. This method is
+ * invoked when a value is evicted to make space, removed by a call to
+ * {@link #remove}, or replaced by a call to {@link #put}. The default
+ * implementation does nothing.
+ *
+ * The method is called without synchronization: other threads may
+ * access the cache while this method is executing.
+ *
+ * @param evicted true if the entry is being removed to make space, false
+ * if the removal was caused by a {@link #put} or {@link #remove}.
+ * @param newValue the new value for {@code key}, if it exists. If non-null,
+ * this removal was caused by a {@link #put}. Otherwise it was caused by
+ * an eviction or a {@link #remove}.
+ */
+ protected open fun entryRemoved(evicted: Boolean, key: K, oldValue: V, newValue: V?) {
+ }
+
+ /**
+ * Called after a cache miss to compute a value for the corresponding key.
+ * Returns the computed value or null if no value can be computed. The
+ * default implementation returns null.
+ *
+ * The method is called without synchronization: other threads may
+ * access the cache while this method is executing.
+ *
+ * If a value for {@code key} exists in the cache when this method
+ * returns, the created value will be released with {@link #entryRemoved}
+ * and discarded. This can occur when multiple threads request the same key
+ * at the same time (causing multiple values to be created), or when one
+ * thread calls {@link #put} while another is creating a value for the same
+ * key.
+ */
+ @JsName("create")
+ protected open fun create(key: K): V? = null
+
+ private fun safeSizeOf(key: K, value: V): Int {
+ val result = sizeOf(key, value)
+ check(result >= 0) { "Negative size: $key=$value" }
+ return result
+ }
+
+ /**
+ * Returns the size of the entry for {@code key} and {@code value} in
+ * user-defined units. The default implementation returns 1 so that size
+ * is the number of entries and max size is the maximum number of entries.
+ *
+ * An entry's size must not change while it is in the cache.
+ */
+ protected open fun sizeOf(key: K, value: V) = 1
+
+ /**
+ * Clear the cache, calling {@link #entryRemoved} on each removed entry.
+ */
+ fun evictAll() {
+ trimToSize(-1) // -1 will evict 0-sized elements
+ }
+
+ /**
+ * For caches that do not override {@link #sizeOf}, this returns the maximum
+ * number of entries in the cache. For all other caches, this returns the
+ * maximum sum of the sizes of the entries in this cache.
+ */
+ fun maxSize(): Int = synchronizedValue { maxSize }
+
+ /**
+ * Returns the number of times {@link #get} returned a value that was
+ * already present in the cache.
+ */
+ fun hitCount(): Int = synchronizedValue { hitCount }
+
+ /**
+ * Returns the number of times {@link #get} returned null or required a new
+ * value to be created.
+ */
+ fun missCount(): Int = synchronizedValue { missCount }
+
+ /**
+ * Returns the number of times {@link #create(Object)} returned a value.
+ */
+ fun createCount(): Int = synchronizedValue { createCount }
+
+ /**
+ * Returns the number of times {@link #put} was called.
+ */
+ fun putCount(): Int = synchronizedValue { putCount }
+
+ /**
+ * Returns the number of values that have been evicted.
+ */
+ fun evictionCount(): Int = synchronizedValue { evictionCount }
+
+ /**
+ * Returns a copy of the current contents of the cache, ordered from least
+ * recently accessed to most recently accessed.
+ */
+ fun snapshot(): Map<K, V> {
+ synchronizedOperation(monitor) {
+ val linkedHashMap = LinkedHashMap<K, V>()
+ for (key in keySet) {
+ linkedHashMap.put(key, map.get(key)!!)
+ }
+ return linkedHashMap
+ }
+ }
+
+ override fun toString(): String {
+ synchronizedOperation(monitor) {
+ val accesses = hitCount + missCount
+ val hitPercent = if (accesses != 0) 100 * hitCount / accesses else 0
+ return "LruCache[maxSize=$maxSize,hits=$hitCount,misses=$missCount," +
+ "hitRate=$hitPercent%]"
+ }
+ }
+
+ internal inline fun <R> synchronizedValue(block: () -> R): R {
+ return synchronizedOperation(monitor, block)
+ }
+}
diff --git a/collection/collection2/src/commonMain/kotlin/androidx/collection/Platform.kt b/collection/collection2/src/commonMain/kotlin/androidx/collection/Platform.kt
new file mode 100644
index 0000000..420910b
--- /dev/null
+++ b/collection/collection2/src/commonMain/kotlin/androidx/collection/Platform.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.collection
+
+internal expect fun indexOutOfBounds(): IndexOutOfBoundsException
+
+/**
+ * Internal class and a temporary solution for the lack of native synchronization intrinsics across
+ * all platforms (read: Apple platforms). On JVM and Android this is just a simple class and the
+ * actual synchronization block use the JVM/Android built-in intrinsics. On other platforms we are
+ * using whatever is currently available.
+ *
+ * TODO(b/172658775): Replace once a solution becomes available.
+ */
+internal expect class SynchronizedObject
+
+internal expect fun createSynchronizedObject(): SynchronizedObject
+
+internal expect inline fun <R> synchronizedOperation(lock: SynchronizedObject, block: () -> R): R
diff --git a/collection/collection2/src/commonMain/kotlin/androidx/collection/SparseArray.kt b/collection/collection2/src/commonMain/kotlin/androidx/collection/SparseArray.kt
new file mode 100644
index 0000000..587c5c4
--- /dev/null
+++ b/collection/collection2/src/commonMain/kotlin/androidx/collection/SparseArray.kt
@@ -0,0 +1,318 @@
+/*
+ * 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.
+ */
+
+/** Avoid conflict (and R8 dup. classes failures) with collection-ktx. */
+// TODO: Remove this after collection-ktx is merged
+@file:JvmName("SparseArray_Ext")
+@file:Suppress("NOTHING_TO_INLINE") // Avoiding additional invocation indirection.
+
+package androidx.collection
+
+import kotlin.jvm.JvmName
+
+expect class SparseArray<E>(initialCapacity: Int = 10) {
+ internal var keys: IntArray
+ internal var values: Array<Any?>
+ internal var garbage: Boolean
+
+ @Suppress("PropertyName") // Normal backing field name but internal for common code.
+ internal var _size: Int
+
+ constructor(array: SparseArray<E>)
+
+ val size: Int
+ fun isEmpty(): Boolean
+
+ operator fun get(key: Int): E?
+ fun get(key: Int, default: E): E
+
+ fun put(key: Int, value: E) // TODO operator
+ fun putAll(other: SparseArray<out E>)
+ fun putIfAbsent(key: Int, value: E): E?
+ fun append(key: Int, value: E)
+
+ fun keyAt(index: Int): Int
+
+ fun valueAt(index: Int): E
+ fun setValueAt(index: Int, value: E)
+
+ fun indexOfKey(key: Int): Int
+ fun indexOfValue(value: E): Int
+
+ fun containsKey(key: Int): Boolean
+ fun containsValue(value: E): Boolean
+
+ fun clear()
+
+ fun remove(key: Int)
+ fun remove(key: Int, value: Any?): Boolean
+ fun removeAt(index: Int)
+
+ fun replace(key: Int, value: E): E?
+ fun replace(key: Int, oldValue: E?, newValue: E): Boolean
+}
+
+internal inline fun <E> SparseArray<E>.commonSize(): Int {
+ if (garbage) {
+ gc()
+ }
+ return _size
+}
+
+internal inline fun <E> SparseArray<E>.commonIsEmpty(): Boolean {
+ return size == 0
+}
+
+internal inline fun <T, E : T> SparseArray<E>.commonGet(key: Int, default: T): T {
+ val i = keys.binarySearch(_size, key)
+ if (i >= 0) {
+ val value = values[i]
+ if (value !== DELETED) {
+ @Suppress("UNCHECKED_CAST") // Guaranteed by positive index and DELETED check.
+ return value as E
+ }
+ }
+ return default
+}
+
+internal inline fun <E> SparseArray<E>.commonPut(key: Int, value: E) {
+ var index = keys.binarySearch(_size, key)
+ if (index >= 0) {
+ values[index] = value
+ } else {
+ index = index.inv()
+ if (index < _size && values[index] === DELETED) {
+ keys[index] = key
+ values[index] = value
+ return
+ }
+ if (garbage && _size >= keys.size) {
+ gc()
+ // Search again because indices may have changed.
+ index = keys.binarySearch(_size, key).inv()
+ }
+ if (_size >= keys.size) {
+ val newSize = idealIntArraySize(_size + 1)
+ keys = keys.copyOf(newSize)
+ values = values.copyOf(newSize)
+ }
+ if (_size - index != 0) {
+ keys.copyInto(keys, destinationOffset = index + 1, startIndex = index, endIndex = _size)
+ values.copyInto(
+ values,
+ destinationOffset = index + 1,
+ startIndex = index,
+ endIndex = _size
+ )
+ }
+ keys[index] = key
+ values[index] = value
+ _size++
+ }
+}
+
+internal inline fun <E> SparseArray<E>.commonPutAll(other: SparseArray<out E>) {
+ for (i in 0 until other.size) {
+ @Suppress("UNCHECKED_CAST") // Guaranteed by valid index.
+ put(other.keys[i], other.values[i] as E)
+ }
+}
+
+internal inline fun <E> SparseArray<E>.commonPutIfAbsent(key: Int, value: E): E? {
+ val mapValue = get(key)
+ if (mapValue == null) {
+ // TODO avoid double binary search here
+ put(key, value)
+ }
+ return mapValue
+}
+
+internal inline fun <E> SparseArray<E>.commonAppend(key: Int, value: E) {
+ if (_size != 0 && key <= keys[_size - 1]) {
+ put(key, value)
+ return
+ }
+ if (garbage && _size >= keys.size) {
+ gc()
+ }
+ val pos = _size
+ if (pos >= keys.size) {
+ val newSize = idealIntArraySize(pos + 1)
+ keys = keys.copyOf(newSize)
+ values = values.copyOf(newSize)
+ }
+ keys[pos] = key
+ values[pos] = value
+ _size = pos + 1
+}
+
+internal inline fun <E> SparseArray<E>.commonKeyAt(index: Int): Int {
+ if (garbage) {
+ gc()
+ }
+ return keys[index]
+}
+
+internal inline fun <E> SparseArray<E>.commonValueAt(index: Int): E {
+ if (garbage) {
+ gc()
+ }
+ @Suppress("UNCHECKED_CAST") // Guaranteed by having run GC.
+ return values[index] as E
+}
+
+internal inline fun <E> SparseArray<E>.commonSetValueAt(index: Int, value: E) {
+ if (garbage) {
+ gc()
+ }
+ values[index] = value
+}
+
+internal inline fun <E> SparseArray<E>.commonIndexOfKey(key: Int): Int {
+ if (garbage) {
+ gc()
+ }
+ return keys.binarySearch(_size, key)
+}
+
+internal inline fun <E> SparseArray<E>.commonIndexOfValue(value: E): Int {
+ if (garbage) {
+ gc()
+ }
+ for (i in 0 until _size) {
+ if (values[i] === value) {
+ return i
+ }
+ }
+ return -1
+}
+
+internal inline fun <E> SparseArray<E>.commonContainsKey(key: Int): Boolean {
+ return indexOfKey(key) >= 0
+}
+
+internal inline fun <E> SparseArray<E>.commonContainsValue(value: E): Boolean {
+ return indexOfValue(value) >= 0
+}
+
+internal inline fun <E> SparseArray<E>.commonClear() {
+ values.fill(null, toIndex = _size)
+ _size = 0
+ garbage = false
+}
+
+internal inline fun <E> SparseArray<E>.commonRemove(key: Int) {
+ val index = keys.binarySearch(_size, key)
+ if (index >= 0 && values[index] !== DELETED) {
+ values[index] = DELETED
+ garbage = true
+ }
+}
+
+internal inline fun <E> SparseArray<E>.commonRemove(key: Int, value: Any?): Boolean {
+ val index = indexOfKey(key)
+ if (index >= 0) {
+ val mapValue = valueAt(index)
+ if (value == mapValue) {
+ removeAt(index)
+ return true
+ }
+ }
+ return false
+}
+
+internal inline fun <E> SparseArray<E>.commonRemoveAt(index: Int) {
+ if (values[index] !== DELETED) {
+ values[index] = DELETED
+ garbage = true
+ }
+}
+
+internal inline fun <E> SparseArray<E>.commonReplace(key: Int, value: E): E? {
+ val index = indexOfKey(key)
+ if (index >= 0) {
+ @Suppress("UNCHECKED_CAST") // Guaranteed by index which would have run GC.
+ val oldValue = values[index] as E
+ values[index] = value
+ return oldValue
+ }
+ return null
+}
+
+internal inline fun <E> SparseArray<E>.commonReplace(key: Int, oldValue: E?, newValue: E): Boolean {
+ val index = indexOfKey(key)
+ if (index >= 0) {
+ val mapValue = values[index]
+ if (mapValue == oldValue) {
+ values[index] = newValue
+ return true
+ }
+ }
+ return false
+}
+
+internal inline fun <E> SparseArray<E>.commonClone(): SparseArray<E> {
+ val new = SparseArray<E>(0)
+ new._size = _size
+ new.keys = keys.copyOf()
+ new.values = values.copyOf()
+ new.garbage = garbage
+ new.gc()
+ return new
+}
+
+internal inline fun <E> SparseArray<E>.commonToString(): String {
+ if (size == 0) {
+ return "{}"
+ }
+ return buildString(_size * 20) {
+ append('{')
+ for (i in 0 until _size) {
+ if (i > 0) {
+ append(", ")
+ }
+ val key = keyAt(i)
+ append(key)
+ append('=')
+ val value = valueAt(i)
+ if (value !== this) {
+ append(value)
+ } else {
+ append("(this Map)")
+ }
+ }
+ append('}')
+ }
+}
+
+internal fun <E> SparseArray<E>.gc() {
+ var newSize = 0
+ val keys = keys
+ val values = values
+ for (i in 0 until _size) {
+ val value = values[i]
+ if (value !== DELETED) {
+ if (i != newSize) {
+ keys[newSize] = keys[i]
+ values[newSize] = value
+ values[i] = null
+ }
+ newSize++
+ }
+ }
+ garbage = false
+ _size = newSize
+}
diff --git a/collection/collection2/src/commonTest/kotlin/androidx/collection/ArrayMapKotlinTest.kt b/collection/collection2/src/commonTest/kotlin/androidx/collection/ArrayMapKotlinTest.kt
new file mode 100644
index 0000000..56ff498
--- /dev/null
+++ b/collection/collection2/src/commonTest/kotlin/androidx/collection/ArrayMapKotlinTest.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class ArrayMapKotlinTest {
+ @Test
+ fun testCanNotIteratePastEnd_entrySetIterator() {
+ val map: MutableMap<String, String> = ArrayMap()
+ map.put("key 1", "value 1")
+ map.put("key 2", "value 2")
+ val expectedEntriesToIterate = mutableSetOf(
+ Pair("key 1", "value 1"),
+ Pair("key 2", "value 2")
+ )
+ val iterator: Iterator<Map.Entry<String, String>> = map.entries.iterator()
+
+ // Assert iteration over the expected two entries in any order
+ assertTrue(iterator.hasNext())
+ val firstEntry = iterator.next().toPair()
+ assertTrue(expectedEntriesToIterate.remove(firstEntry))
+ assertTrue(iterator.hasNext())
+ val secondEntry = iterator.next().toPair()
+ assertTrue(expectedEntriesToIterate.remove(secondEntry))
+ assertFalse(iterator.hasNext())
+ assertTrue(runCatching { iterator.next() }.exceptionOrNull() is NoSuchElementException)
+ }
+
+ @Test
+ fun testCanNotIteratePastEnd_keySetIterator() {
+ val map: MutableMap<String, String> = ArrayMap()
+ map.put("key 1", "value 1")
+ map.put("key 2", "value 2")
+ val expectedKeysToIterate = mutableSetOf("key 1", "key 2")
+ val iterator = map.keys.iterator()
+
+ // Assert iteration over the expected two keys in any order
+ assertTrue(iterator.hasNext())
+ val firstKey = iterator.next()
+ assertTrue(expectedKeysToIterate.remove(firstKey))
+ assertTrue(iterator.hasNext())
+ val secondKey = iterator.next()
+ assertTrue(expectedKeysToIterate.remove(secondKey))
+ assertFalse(iterator.hasNext())
+ assertTrue(runCatching { iterator.next() }.exceptionOrNull() is NoSuchElementException)
+ }
+
+ @Test
+ fun testCanNotIteratePastEnd_valuesIterator() {
+ val map: MutableMap<String, String> = ArrayMap()
+ map.put("key 1", "value 1")
+ map.put("key 2", "value 2")
+ val expectedValuesToIterate = mutableSetOf("value 1", "value 2")
+ val iterator = map.values.iterator()
+
+ // Assert iteration over the expected two values in any order
+ assertTrue(iterator.hasNext())
+ val firstValue = iterator.next()
+ assertTrue(expectedValuesToIterate.remove(firstValue))
+ assertTrue(iterator.hasNext())
+ val secondValue = iterator.next()
+ assertTrue(expectedValuesToIterate.remove(secondValue))
+ assertFalse(iterator.hasNext())
+ assertTrue(runCatching { iterator.next() }.exceptionOrNull() is NoSuchElementException)
+ }
+
+ /**
+ * Check to make sure the same operations behave as expected in a single thread.
+ */
+ @Test
+ fun testNonConcurrentAccesses() {
+ val mMap = ArrayMap<String, String>()
+ var i = 0
+ while (i < 100000) {
+ mMap["key $i"] = "B_DONT_DO_THAT"
+ i++
+ if (i % 200 == 0) {
+ print(".")
+ }
+ if (i % 500 == 0) {
+ mMap.clear()
+ print("X")
+ }
+ i++
+ }
+ // Test is expected to pass without throwing anything.
+ }
+}
\ No newline at end of file
diff --git a/collection/collection2/src/commonTest/kotlin/androidx/collection/ArraySetKotlinTest.kt b/collection/collection2/src/commonTest/kotlin/androidx/collection/ArraySetKotlinTest.kt
new file mode 100644
index 0000000..555081d
--- /dev/null
+++ b/collection/collection2/src/commonTest/kotlin/androidx/collection/ArraySetKotlinTest.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+class ArraySetKotlinTest {
+ @Test
+ fun testCanNotIteratePastEnd() {
+ val set = ArraySet<String>()
+ set.add("value")
+ val iterator: Iterator<String> = set.iterator()
+ assertTrue(iterator.hasNext())
+ assertEquals("value", iterator.next())
+ assertFalse(iterator.hasNext())
+ assertTrue(runCatching { iterator.next() }.exceptionOrNull() is NoSuchElementException)
+ }
+
+ /**
+ * Check to make sure the same operations behave as expected in a single thread.
+ */
+ @Test
+ fun testNonConcurrentAccesses() {
+ val set = ArraySet<String>()
+ var i = 0
+ while (i < 100000) {
+ try {
+ set.add("key $i")
+ i++
+ if (i % 200 == 0) {
+ print(".")
+ }
+ if (i % 500 == 0) {
+ set.clear()
+ }
+ } catch (e: ConcurrentModificationException) {
+ fail(cause = e)
+ }
+ i++
+ }
+ }
+
+ @Test fun addAllTypeProjection() = testBody {
+ val set1 = ArraySet<Any>()
+ val set2 = ArraySet<String>()
+ set2.add("Foo")
+ set2.add("Bar")
+ set1.addAll(set2)
+ assertEquals(set1.asIterable().toSet(), setOf("Bar", "Foo"))
+ }
+
+ /**
+ * Test for implementation correction. This makes sure that all branches in ArraySet's
+ * backstore shrinking code gets exercised.
+ */
+ @Test
+ fun addAllThenRemoveOneByOne() {
+ val sourceList = (0 until 10).toList()
+ val set = ArraySet(sourceList)
+ assertEquals(sourceList.size, set.size)
+
+ for (e in sourceList) {
+ assertTrue(set.contains(e))
+ set.remove(e)
+ }
+ assertTrue(set.isEmpty())
+ }
+
+ /**
+ * Test for implementation correction of indexOf.
+ */
+ @Test
+ fun addObjectsWithSameHashCode() {
+ class Value {
+ override fun hashCode(): Int {
+ return 42
+ }
+ }
+
+ val sourceList = (0 until 10).map { Value() }
+ val set = ArraySet(sourceList)
+ assertEquals(sourceList.size, set.size)
+
+ for (e in sourceList) {
+ assertEquals(e, set.valueAt(set.indexOf(e)))
+ }
+ }
+}
diff --git a/collection/collection2/src/commonTest/kotlin/androidx/collection/CircularArrayKotlinTest.kt b/collection/collection2/src/commonTest/kotlin/androidx/collection/CircularArrayKotlinTest.kt
new file mode 100644
index 0000000..48c4b16
--- /dev/null
+++ b/collection/collection2/src/commonTest/kotlin/androidx/collection/CircularArrayKotlinTest.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+class CircularArrayKotlinTest {
+ val ELEMENT_X = Any()
+ val ELEMENT_Y = Any()
+ val ELEMENT_Z = Any()
+
+ @Test
+ fun creatingWithZeroCapacity() {
+ assertThrows<IllegalArgumentException> { CircularArray<Any>(0) }
+ }
+
+ @Test
+ fun creatingWithOverCapacity() {
+ assertThrows<IllegalArgumentException> { CircularArray<Any>(Int.MAX_VALUE) }
+ }
+
+ @Test
+ fun basicOperations() {
+ val array = CircularArray<Any>()
+ assertTrue(array.isEmpty())
+ array.addFirst(ELEMENT_X)
+ array.addFirst(ELEMENT_Y)
+ array.addLast(ELEMENT_Z)
+ assertFalse(array.isEmpty())
+ assertSame(ELEMENT_Y, array.first)
+ assertSame(ELEMENT_Z, array.last)
+ assertSame(ELEMENT_X, array[1])
+ assertSame(ELEMENT_Y, array.popFirst())
+ assertSame(ELEMENT_Z, array.popLast())
+ assertSame(ELEMENT_X, array.first)
+ assertSame(ELEMENT_X, array.last)
+ assertSame(ELEMENT_X, array.popFirst())
+ assertTrue(array.isEmpty())
+
+ assertThrows<IndexOutOfBoundsException> { array.popFirst() }
+ }
+
+ @Test
+ fun removeFromEitherEnd() {
+ val array = CircularArray<Any>()
+ array.addFirst(ELEMENT_X)
+ array.addFirst(ELEMENT_Y)
+ array.addLast(ELEMENT_Z)
+
+ // These are no-ops.
+ array.removeFromStart(0)
+ array.removeFromStart(-1)
+ array.removeFromEnd(0)
+ array.removeFromEnd(-1)
+
+ assertThrows<IndexOutOfBoundsException> { array.removeFromStart(4) }
+ assertThrows<IndexOutOfBoundsException> { array.removeFromEnd(4) }
+
+ array.removeFromStart(2)
+ assertSame(ELEMENT_Z, array.first)
+ array.removeFromEnd(1)
+ assertTrue(array.isEmpty())
+ }
+
+ @Test
+ fun grow() {
+ val array = CircularArray<Any>(1)
+ val expectedSize = 32768
+ for (i in 0 until expectedSize) {
+ array.addFirst(i)
+ }
+ assertEquals(expectedSize, array.size)
+ }
+}
diff --git a/collection/collection2/src/commonTest/kotlin/androidx/collection/CircularIntArrayKotlinTest.kt b/collection/collection2/src/commonTest/kotlin/androidx/collection/CircularIntArrayKotlinTest.kt
new file mode 100644
index 0000000..10760e5
--- /dev/null
+++ b/collection/collection2/src/commonTest/kotlin/androidx/collection/CircularIntArrayKotlinTest.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class CircularIntArrayKotlinTest {
+ @Test
+ fun creatingWithZeroCapacity() {
+ assertThrows<IllegalArgumentException> { CircularIntArray(0) }
+ }
+
+ @Test
+ fun creatingWithOverCapacity() {
+ assertThrows<IllegalArgumentException> { CircularIntArray(Int.MAX_VALUE) }
+ }
+
+ @Test
+ fun basicOperations() {
+ val array = CircularIntArray()
+ assertTrue(array.isEmpty())
+ array.addFirst(42)
+ array.addFirst(43)
+ array.addLast(-1)
+ assertFalse(array.isEmpty())
+ assertEquals(43, array.first)
+ assertEquals(-1, array.last)
+ assertEquals(42, array[1])
+ assertEquals(43, array.popFirst())
+ assertEquals(-1, array.popLast())
+ assertEquals(42, array.first)
+ assertEquals(42, array.last)
+ assertEquals(42, array.popFirst())
+ assertTrue(array.isEmpty())
+
+ assertThrows<IndexOutOfBoundsException> { array.popFirst() }
+ }
+
+ @Test
+ fun removeFromEitherEnd() {
+ val array = CircularIntArray()
+ array.addFirst(42)
+ array.addFirst(43)
+ array.addLast(-1)
+
+ // These are no-ops.
+ array.removeFromStart(0)
+ array.removeFromStart(-1)
+ array.removeFromEnd(0)
+ array.removeFromEnd(-1)
+
+ assertThrows<IndexOutOfBoundsException> { array.removeFromStart(4) }
+ assertThrows<IndexOutOfBoundsException> { array.removeFromEnd(4) }
+
+ array.removeFromStart(2)
+ assertEquals(-1, array.first)
+ array.removeFromEnd(1)
+ assertTrue(array.isEmpty())
+ }
+
+ @Test
+ fun grow() {
+ val array = CircularIntArray(1)
+ val expectedSize = 32768
+ for (i in 0 until expectedSize) {
+ array.addFirst(i)
+ }
+ assertEquals(expectedSize, array.size)
+ }
+}
\ No newline at end of file
diff --git a/collection/collection2/src/commonTest/kotlin/androidx/collection/IndexBasedMutableIteratorKotlinTest.kt b/collection/collection2/src/commonTest/kotlin/androidx/collection/IndexBasedMutableIteratorKotlinTest.kt
new file mode 100644
index 0000000..0782e18
--- /dev/null
+++ b/collection/collection2/src/commonTest/kotlin/androidx/collection/IndexBasedMutableIteratorKotlinTest.kt
@@ -0,0 +1,119 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class IndexBasedMutableIteratorKotlinTest {
+
+ @Test
+ fun iterateAll() {
+ val iterator = ArraySet(setOf("a", "b", "c")).iterator()
+ assertEquals(iterator.asSequence().toSet(), setOf("a", "b", "c"))
+ }
+
+ @Test
+ fun iterateEmptyList() {
+ val iterator = ArraySet<String>().iterator()
+ assertFalse(iterator.hasNext())
+ assertTrue(
+ runCatching {
+ iterator.next()
+ }.exceptionOrNull() is NoSuchElementException
+ )
+ }
+
+ @Test
+ fun removeSameItemTwice() {
+ val iterator = ArraySet(listOf("a", "b", "c")).iterator()
+ iterator.next() // move to next
+ iterator.remove()
+ assertTrue(
+ runCatching {
+ iterator.remove()
+ }.exceptionOrNull() is IllegalStateException
+ )
+ }
+
+ @Test
+ fun removeLast() = removeViaIterator(
+ original = setOf("a", "b", "c"),
+ toBeRemoved = setOf("c"),
+ expected = setOf("a", "b")
+ )
+
+ @Test
+ fun removeFirst() = removeViaIterator(
+ original = setOf("a", "b", "c"),
+ toBeRemoved = setOf("a"),
+ expected = setOf("b", "c")
+ )
+
+ @Test
+ fun removeMid() = removeViaIterator(
+ original = setOf("a", "b", "c"),
+ toBeRemoved = setOf("b"),
+ expected = setOf("a", "c")
+ )
+
+ @Test
+ fun removeConsecutive() = removeViaIterator(
+ original = setOf("a", "b", "c", "d"),
+ toBeRemoved = setOf("b", "c"),
+ expected = setOf("a", "d")
+ )
+
+ @Test
+ fun removeLastTwo() = removeViaIterator(
+ original = setOf("a", "b", "c", "d"),
+ toBeRemoved = setOf("c", "d"),
+ expected = setOf("a", "b")
+ )
+
+ @Test
+ fun removeFirstTwo() = removeViaIterator(
+ original = setOf("a", "b", "c", "d"),
+ toBeRemoved = setOf("a", "b"),
+ expected = setOf("c", "d")
+ )
+
+ @Test
+ fun removeMultiple() = removeViaIterator(
+ original = setOf("a", "b", "c", "d"),
+ toBeRemoved = setOf("a", "c"),
+ expected = setOf("b", "d")
+ )
+
+ private fun removeViaIterator(
+ original: Set<String>,
+ toBeRemoved: Set<String>,
+ expected: Set<String>
+ ) {
+ val subject = ArraySet(original)
+ val iterator = subject.iterator()
+ while (iterator.hasNext()) {
+ val next = iterator.next()
+ if (next in toBeRemoved) {
+ iterator.remove()
+ }
+ }
+ assertTrue(subject.size == expected.size && subject.containsAll(expected))
+ }
+}
diff --git a/collection/collection2/src/commonTest/kotlin/androidx/collection/LongSparseArrayKotlinTest.kt b/collection/collection2/src/commonTest/kotlin/androidx/collection/LongSparseArrayKotlinTest.kt
new file mode 100644
index 0000000..d739d9b3
--- /dev/null
+++ b/collection/collection2/src/commonTest/kotlin/androidx/collection/LongSparseArrayKotlinTest.kt
@@ -0,0 +1,283 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotSame
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+class LongSparseArrayKotlinTest {
+ @Test
+ fun getOrDefaultPrefersStoredValue() {
+ val map = LongSparseArray<String>()
+ map.put(1L, "1")
+ assertEquals("1", map.get(1L, "2"))
+ }
+
+ @Test
+ fun getOrDefaultUsesDefaultWhenAbsent() {
+ val map = LongSparseArray<String>()
+ assertEquals("1", map.get(1L, "1"))
+ }
+
+ @Test
+ fun getOrDefaultReturnsNullWhenNullStored() {
+ val map = LongSparseArray<String?>()
+ map.put(1L, null)
+ assertNull(map.get(1L, "1"))
+ }
+
+ @Test
+ fun getOrDefaultDoesNotPersistDefault() {
+ val map = LongSparseArray<String>()
+ map.get(1L, "1")
+ assertFalse(map.containsKey(1L))
+ }
+
+ @Test
+ fun putIfAbsentDoesNotOverwriteStoredValue() {
+ val map = LongSparseArray<String>()
+ map.put(1L, "1")
+ map.putIfAbsent(1L, "2")
+ assertEquals("1", map[1L])
+ }
+
+ @Test
+ fun putIfAbsentReturnsStoredValue() {
+ val map = LongSparseArray<String>()
+ map.put(1L, "1")
+ assertEquals("1", map.putIfAbsent(1L, "2"))
+ }
+
+ @Test
+ fun putIfAbsentStoresValueWhenAbsent() {
+ val map = LongSparseArray<String>()
+ map.putIfAbsent(1L, "2")
+ assertEquals("2", map[1L])
+ }
+
+ @Test
+ fun putIfAbsentReturnsNullWhenAbsent() {
+ val map = LongSparseArray<String>()
+ assertNull(map.putIfAbsent(1L, "2"))
+ }
+
+ @Test
+ fun replaceWhenAbsentDoesNotStore() {
+ val map = LongSparseArray<String>()
+ assertNull(map.replace(1L, "1"))
+ assertFalse(map.containsKey(1L))
+ }
+
+ @Test
+ fun replaceStoresAndReturnsOldValue() {
+ val map = LongSparseArray<String>()
+ map.put(1L, "1")
+ assertEquals("1", map.replace(1L, "2"))
+ assertEquals("2", map[1L])
+ }
+
+ @Test
+ fun replaceStoresAndReturnsNullWhenMappedToNull() {
+ val map = LongSparseArray<String?>()
+ map.put(1L, null)
+ assertNull(map.replace(1L, "1"))
+ assertEquals("1", map[1L])
+ }
+
+ @Test
+ fun replaceValueKeyAbsent() {
+ val map = LongSparseArray<String>()
+ assertFalse(map.replace(1L, "1", "2"))
+ assertFalse(map.containsKey(1L))
+ }
+
+ @Test
+ fun replaceValueMismatchDoesNotReplace() {
+ val map = LongSparseArray<String>()
+ map.put(1L, "1")
+ assertFalse(map.replace(1L, "2", "3"))
+ assertEquals("1", map[1L])
+ }
+
+ @Test
+ fun replaceValueMismatchNullDoesNotReplace() {
+ val map = LongSparseArray<String>()
+ map.put(1L, "1")
+ assertFalse(map.replace(1L, null, "2"))
+ assertEquals("1", map[1L])
+ }
+
+ @Test
+ fun replaceValueMatchReplaces() {
+ val map = LongSparseArray<String>()
+ map.put(1L, "1")
+ assertTrue(map.replace(1L, "1", "2"))
+ assertEquals("2", map[1L])
+ }
+
+ @Test
+ fun replaceNullValueMismatchDoesNotReplace() {
+ val map = LongSparseArray<String?>()
+ map.put(1L, null)
+ assertFalse(map.replace(1L, "1", "2"))
+ assertNull(map[1L])
+ }
+
+ @Test
+ fun replaceNullValueMatchRemoves() {
+ val map = LongSparseArray<String?>()
+ map.put(1L, null)
+ assertTrue(map.replace(1L, null, "1"))
+ assertEquals("1", map[1L])
+ }
+
+ @Test
+ fun removeValueKeyAbsent() {
+ val map = LongSparseArray<String>()
+ assertFalse(map.remove(1L, "1"))
+ }
+
+ @Test
+ fun removeValueMismatchDoesNotRemove() {
+ val map = LongSparseArray<String>()
+ map.put(1L, "1")
+ assertFalse(map.remove(1L, "2"))
+ assertTrue(map.containsKey(1L))
+ }
+
+ @Test
+ fun removeValueMismatchNullDoesNotRemove() {
+ val map = LongSparseArray<String>()
+ map.put(1L, "1")
+ assertFalse(map.remove(1L, null))
+ assertTrue(map.containsKey(1L))
+ }
+
+ @Test
+ fun removeValueMatchRemoves() {
+ val map = LongSparseArray<String>()
+ map.put(1L, "1")
+ assertTrue(map.remove(1L, "1"))
+ assertFalse(map.containsKey(1L))
+ }
+
+ @Test
+ fun removeNullValueMismatchDoesNotRemove() {
+ val map = LongSparseArray<String?>()
+ map.put(1L, null)
+ assertFalse(map.remove(1L, "2"))
+ assertTrue(map.containsKey(1L))
+ }
+
+ @Test
+ fun removeNullValueMatchRemoves() {
+ val map = LongSparseArray<String?>()
+ map.put(1L, null)
+ assertTrue(map.remove(1L, null))
+ assertFalse(map.containsKey(1L))
+ }
+
+ @Test
+ fun isEmpty() {
+ val LongSparseArray = LongSparseArray<String>()
+ assertTrue(LongSparseArray.isEmpty()) // Newly created LongSparseArray should be empty
+
+ // Adding elements should change state from empty to not empty.
+ for (i in 0L..5L - 1) {
+ LongSparseArray.put(i, i.toString())
+ assertFalse(LongSparseArray.isEmpty())
+ }
+ LongSparseArray.clear()
+ assertTrue(LongSparseArray.isEmpty()) // A cleared LongSparseArray should be empty.
+ val key1 = 1L
+ val key2 = 2L
+ val value1 = "some value"
+ val value2 = "some other value"
+ LongSparseArray.append(key1, value1)
+ assertFalse(LongSparseArray.isEmpty()) // has 1 element.
+ LongSparseArray.append(key2, value2)
+ assertFalse(LongSparseArray.isEmpty()) // has 2 elements.
+ assertFalse(LongSparseArray.isEmpty()) // consecutive calls should be OK.
+ LongSparseArray.remove(key1)
+ assertFalse(LongSparseArray.isEmpty()) // has 1 element.
+ LongSparseArray.remove(key2)
+ assertTrue(LongSparseArray.isEmpty())
+ }
+
+ @Test
+ fun containsKey() {
+ val array = LongSparseArray<String>()
+ array.put(1L, "one")
+ assertTrue(array.containsKey(1L))
+ assertFalse(array.containsKey(2L))
+ }
+
+ @Test
+ fun containsValue() {
+ val array = LongSparseArray<String>()
+ array.put(1L, "one")
+ assertTrue(array.containsValue("one"))
+ assertFalse(array.containsValue("two"))
+ }
+
+ @Test
+ fun putAll() {
+ val dest = LongSparseArray<String>()
+ dest.put(1L, "one")
+ dest.put(3L, "three")
+ val source = LongSparseArray<String>()
+ source.put(1L, "uno")
+ source.put(2L, "dos")
+ dest.putAll(source)
+ assertEquals(3, dest.size)
+ assertEquals("uno", dest[1L])
+ assertEquals("dos", dest[2L])
+ assertEquals("three", dest[3L])
+ }
+
+ @Test
+ fun putAllVariance() {
+ val dest = LongSparseArray<Any>()
+ dest.put(1L, 1L)
+ val source = LongSparseArray<String>()
+ dest.put(2L, "two")
+ dest.putAll(source)
+ assertEquals(2, dest.size)
+ assertEquals(1L, dest[1L])
+ assertEquals("two", dest[2L])
+ }
+
+ @Test
+ fun copyConstructor() {
+ val source = LongSparseArray<String>()
+ source.put(42, "cuarenta y dos")
+ source.put(3, "tres")
+ source.put(11, "once")
+ val dest = LongSparseArray(source)
+ assertNotSame(source, dest)
+
+ assertEquals(source.size, dest.size)
+ for (i in 0 until source.size) {
+ assertEquals(source.keyAt(i), dest.keyAt(i))
+ assertEquals(source.valueAt(i), dest.valueAt(i))
+ }
+ }
+}
\ No newline at end of file
diff --git a/collection/collection2/src/commonTest/kotlin/androidx/collection/LruCacheKotlinTest.kt b/collection/collection2/src/commonTest/kotlin/androidx/collection/LruCacheKotlinTest.kt
new file mode 100644
index 0000000..4c239b2
--- /dev/null
+++ b/collection/collection2/src/commonTest/kotlin/androidx/collection/LruCacheKotlinTest.kt
@@ -0,0 +1,459 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class LruCacheKotlinTest {
+ private var expectedCreateCount = 0
+ private var expectedPutCount = 0
+ private var expectedHitCount = 0
+ private var expectedMissCount = 0
+ private var expectedEvictionCount = 0
+
+ @Test
+ fun testStatistics() {
+ val cache = LruCache<String, String>(3)
+ assertStatistics(cache)
+ assertEquals(null, cache.put("a", "A"))
+ expectedPutCount++
+ assertStatistics(cache)
+ assertHit(cache, "a", "A")
+ assertSnapshot(cache, "a", "A")
+ assertEquals(null, cache.put("b", "B"))
+ expectedPutCount++
+ assertStatistics(cache)
+ assertHit(cache, "a", "A")
+ assertHit(cache, "b", "B")
+ assertSnapshot(cache, "a", "A", "b", "B")
+ assertEquals(null, cache.put("c", "C"))
+ expectedPutCount++
+ assertStatistics(cache)
+ assertHit(cache, "a", "A")
+ assertHit(cache, "b", "B")
+ assertHit(cache, "c", "C")
+ assertSnapshot(cache, "a", "A", "b", "B", "c", "C")
+ assertEquals(null, cache.put("d", "D"))
+ expectedPutCount++
+ expectedEvictionCount++ // a should have been evicted
+ assertStatistics(cache)
+ assertMiss(cache, "a")
+ assertHit(cache, "b", "B")
+ assertHit(cache, "c", "C")
+ assertHit(cache, "d", "D")
+ assertHit(cache, "b", "B")
+ assertHit(cache, "c", "C")
+ assertSnapshot(cache, "d", "D", "b", "B", "c", "C")
+ assertEquals(null, cache.put("e", "E"))
+ expectedPutCount++
+ expectedEvictionCount++ // d should have been evicted
+ assertStatistics(cache)
+ assertMiss(cache, "d")
+ assertMiss(cache, "a")
+ assertHit(cache, "e", "E")
+ assertHit(cache, "b", "B")
+ assertHit(cache, "c", "C")
+ assertSnapshot(cache, "e", "E", "b", "B", "c", "C")
+ }
+
+ @Test
+ fun testStatisticsWithCreate() {
+ val cache = newCreatingCache()
+ assertStatistics(cache)
+ assertCreated(cache, "aa", "created-aa")
+ assertHit(cache, "aa", "created-aa")
+ assertSnapshot(cache, "aa", "created-aa")
+ assertCreated(cache, "bb", "created-bb")
+ assertMiss(cache, "c")
+ assertSnapshot(cache, "aa", "created-aa", "bb", "created-bb")
+ assertCreated(cache, "cc", "created-cc")
+ assertSnapshot(cache, "aa", "created-aa", "bb", "created-bb", "cc", "created-cc")
+ expectedEvictionCount++ // aa will be evicted
+ assertCreated(cache, "dd", "created-dd")
+ assertSnapshot(cache, "bb", "created-bb", "cc", "created-cc", "dd", "created-dd")
+ expectedEvictionCount++ // bb will be evicted
+ assertCreated(cache, "aa", "created-aa")
+ assertSnapshot(cache, "cc", "created-cc", "dd", "created-dd", "aa", "created-aa")
+ }
+
+ @Test
+ fun testCreateOnCacheMiss() {
+ val cache = newCreatingCache()
+ val created = cache.get("aa")
+ assertEquals("created-aa", created)
+ }
+
+ @Test
+ fun testNoCreateOnCacheHit() {
+ val cache = newCreatingCache()
+ cache.put("aa", "put-aa")
+ assertEquals("put-aa", cache.get("aa"))
+ }
+
+ @Test
+ fun testConstructorDoesNotAllowZeroCacheSize() {
+ assertThrows<IllegalArgumentException> { LruCache<String, String>(0) }
+ }
+
+ @Test
+ fun testToString() {
+ val cache = LruCache<String, String>(3)
+ assertEquals(
+ "LruCache[maxSize=3,hits=0,misses=0,hitRate=0%]",
+ cache.toString()
+ )
+ cache.put("a", "A")
+ cache.put("b", "B")
+ cache.put("c", "C")
+ cache.put("d", "D")
+ cache.get("a") // miss
+ cache.get("b") // hit
+ cache.get("c") // hit
+ cache.get("d") // hit
+ cache.get("e") // miss
+ assertEquals(
+ "LruCache[maxSize=3,hits=3,misses=2,hitRate=60%]",
+ cache.toString()
+ )
+ }
+
+ @Test
+ fun testEvictionWithSingletonCache() {
+ val cache = LruCache<String, String>(1)
+ cache.put("a", "A")
+ cache.put("b", "B")
+ assertSnapshot(cache, "b", "B")
+ }
+
+ @Test
+ fun testEntryEvictedWhenFull() {
+ val log = mutableListOf<String>()
+ val cache = newRemovalLogCache(log)
+ cache.put("a", "A")
+ cache.put("b", "B")
+ cache.put("c", "C")
+ assertEquals(emptyList<String>(), log)
+ cache.put("d", "D")
+ assertEquals(listOf("a=A"), log)
+ }
+
+ /**
+ * Replacing the value for a key doesn't cause an eviction but it does bring
+ * the replaced entry to the front of the queue.
+ */
+ @Test
+ fun testPutCauseEviction() {
+ val log = mutableListOf<String>()
+ val cache = newRemovalLogCache(log)
+ cache.put("a", "A")
+ cache.put("b", "B")
+ cache.put("c", "C")
+ cache.put("b", "B2")
+ assertEquals(listOf("b=B>B2"), log)
+ assertSnapshot(cache, "a", "A", "c", "C", "b", "B2")
+ }
+
+ @Test
+ fun testCustomSizesImpactsSize() {
+ val cache = object : LruCache<String, String>(10) {
+ override fun sizeOf(key: String, value: String): Int {
+ return key.length + value.length
+ }
+ }
+ assertEquals(0, cache.size)
+ cache.put("a", "AA")
+ assertEquals(3, cache.size)
+ cache.put("b", "BBBB")
+ assertEquals(8, cache.size)
+ cache.put("a", "")
+ assertEquals(6, cache.size)
+ }
+
+ @Test
+ fun testEvictionWithCustomSizes() {
+ val cache = object : LruCache<String, String>(4) {
+ override fun sizeOf(key: String, value: String): Int {
+ return value.length
+ }
+ }
+ cache.put("a", "AAAA")
+ assertSnapshot(cache, "a", "AAAA")
+ cache.put("b", "BBBB") // should evict a
+ assertSnapshot(cache, "b", "BBBB")
+ cache.put("c", "CC") // should evict b
+ assertSnapshot(cache, "c", "CC")
+ cache.put("d", "DD")
+ assertSnapshot(cache, "c", "CC", "d", "DD")
+ cache.put("e", "E") // should evict c
+ assertSnapshot(cache, "d", "DD", "e", "E")
+ cache.put("f", "F")
+ assertSnapshot(cache, "d", "DD", "e", "E", "f", "F")
+ cache.put("g", "G") // should evict d
+ assertSnapshot(cache, "e", "E", "f", "F", "g", "G")
+ cache.put("h", "H")
+ assertSnapshot(cache, "e", "E", "f", "F", "g", "G", "h", "H")
+ cache.put("i", "III") // should evict e, f, and g
+ assertSnapshot(cache, "h", "H", "i", "III")
+ cache.put("j", "JJJ") // should evict h and i
+ assertSnapshot(cache, "j", "JJJ")
+ }
+
+ @Test
+ fun testEvictionThrowsWhenSizesAreInconsistent() {
+ val cache = object : LruCache<String, IntArray>(4) {
+ override fun sizeOf(key: String, value: IntArray): Int {
+ return value[0]
+ }
+ }
+ val a = intArrayOf(4)
+ cache.put("a", a)
+ // get the cache size out of sync
+ a[0] = 1
+ assertEquals(4, cache.size)
+
+ // evict something
+ assertThrows<IllegalStateException> {
+ cache.put("b", intArrayOf(2))!!
+ }
+ }
+
+ @Test
+ fun testEvictionThrowsWhenSizesAreNegative() {
+ val cache = object : LruCache<String, String>(4) {
+ override fun sizeOf(key: String, value: String): Int {
+ return -1
+ }
+ }
+ assertThrows<IllegalStateException> {
+ cache.put("a", "A")!!
+ }
+ }
+
+ /**
+ * Naive caches evict at most one element at a time. This is problematic
+ * because evicting a small element may be insufficient to make room for a
+ * large element.
+ */
+ @Test
+ fun testDifferentElementSizes() {
+ val cache = object : LruCache<String, String>(10) {
+ override fun sizeOf(key: String, value: String): Int {
+ return value.length
+ }
+ }
+ cache.put("a", "1")
+ cache.put("b", "12345678")
+ cache.put("c", "1")
+ assertSnapshot(cache, "a", "1", "b", "12345678", "c", "1")
+ cache.put("d", "12345678") // should evict a and b
+ assertSnapshot(cache, "c", "1", "d", "12345678")
+ cache.put("e", "12345678") // should evict c and d
+ assertSnapshot(cache, "e", "12345678")
+ }
+
+ @Test
+ fun testEvict() {
+ val log = mutableListOf<String>()
+ val cache = newRemovalLogCache(log)
+ cache.put("a", "A")
+ cache.put("b", "B")
+ cache.put("c", "C")
+ cache.evictAll()
+ assertEquals(0, cache.size)
+ assertEquals(listOf("a=A", "b=B", "c=C"), log)
+ }
+
+ @Test
+ fun testEvictAllEvictsSizeZeroElements() {
+ val cache = object : LruCache<String, String>(10) {
+ override fun sizeOf(key: String, value: String): Int {
+ return 0
+ }
+ }
+ cache.put("a", "A")
+ cache.put("b", "B")
+ cache.evictAll()
+ assertSnapshot(cache)
+ }
+
+ @Test
+ fun testRemoveWithCustomSizes() {
+ val cache = object : LruCache<String, String>(10) {
+ override fun sizeOf(key: String, value: String): Int {
+ return value.length
+ }
+ }
+ cache.put("a", "123456")
+ cache.put("b", "1234")
+ cache.remove("a")
+ assertEquals(4, cache.size)
+ }
+
+ @Test
+ fun testRemoveAbsentElement() {
+ val cache = LruCache<String, String>(10)
+ cache.put("a", "A")
+ cache.put("b", "B")
+ assertEquals(null, cache.remove("c"))
+ assertEquals(2, cache.size)
+ }
+
+ @Test
+ fun testRemoveNullThrows() {
+ val cache = LruCache<String?, String>(10)
+ assertThrows<NullPointerException> { cache.remove(null)!! }
+ }
+
+ @Test
+ fun testRemoveCallsEntryRemoved() {
+ val log = mutableListOf<String>()
+ val cache = newRemovalLogCache(log)
+ cache.put("a", "A")
+ cache.remove("a")
+ assertEquals(listOf("a=A>null"), log)
+ }
+
+ @Test
+ fun testPutCallsEntryRemoved() {
+ val log = mutableListOf<String>()
+ val cache = newRemovalLogCache(log)
+ cache.put("a", "A")
+ cache.put("a", "A2")
+ assertEquals(listOf("a=A>A2"), log)
+ }
+
+ /**
+ * Test what happens when a value is added to the map while create is
+ * working. The map value should be returned by get(), and the created value
+ * should be released with entryRemoved().
+ */
+ @Test
+ fun testCreateWithConcurrentPut() {
+ val log = mutableListOf<String>()
+ val cache = object : LruCache<String, String>(3) {
+ override fun create(key: String): String {
+ put(key, "B")
+ return "A"
+ }
+
+ @Suppress("UNUSED_PARAMETER")
+ override fun entryRemoved(
+ evicted: Boolean,
+ key: String,
+ oldValue: String,
+ newValue: String?
+ ) {
+ log.add("$key=$oldValue>$newValue")
+ }
+ }
+ assertEquals("B", cache.get("a"))
+ assertEquals(listOf("a=A>B"), log)
+ }
+
+ /**
+ * Test what happens when two creates happen concurrently. The result from
+ * the first create to return is returned by both gets. The other created
+ * values should be released with entryRemove().
+ */
+ @Test
+ fun testCreateWithConcurrentCreate() {
+ val log = mutableListOf<String>()
+ val cache = object : LruCache<String, Int>(3) {
+ var mCallCount = 0
+ override fun create(key: String): Int {
+ return if (mCallCount++ == 0) {
+ assertEquals(2, get(key))
+ 1
+ } else {
+ 2
+ }
+ }
+
+ @Suppress("UNUSED_PARAMETER")
+ override fun entryRemoved(
+ evicted: Boolean,
+ key: String,
+ oldValue: Int,
+ newValue: Int?
+ ) {
+ log.add("$key=$oldValue>$newValue")
+ }
+ }
+ assertEquals(2, cache.get("a"))
+ assertEquals(listOf("a=1>2"), log)
+ }
+
+ private fun newCreatingCache(): LruCache<String, String> {
+ return object : LruCache<String, String>(3) {
+ override fun create(key: String): String? {
+ return if (key.length > 1) "created-$key" else null
+ }
+ }
+ }
+
+ private fun newRemovalLogCache(log: MutableList<String>): LruCache<String, String> {
+ return object : LruCache<String, String>(3) {
+ override fun entryRemoved(
+ evicted: Boolean,
+ key: String,
+ oldValue: String,
+ newValue: String?
+ ) {
+ val message = if (evicted) "$key=$oldValue" else "$key=$oldValue>$newValue"
+ log.add(message)
+ }
+ }
+ }
+
+ private fun assertHit(cache: LruCache<String, String>, key: String, value: String) {
+ assertEquals(value, cache.get(key))
+ expectedHitCount++
+ assertStatistics(cache)
+ }
+
+ private fun assertMiss(cache: LruCache<String, String>, key: String) {
+ assertEquals(null, cache.get(key))
+ expectedMissCount++
+ assertStatistics(cache)
+ }
+
+ private fun assertCreated(cache: LruCache<String, String>, key: String, value: String) {
+ assertEquals(value, cache.get(key))
+ expectedMissCount++
+ expectedCreateCount++
+ assertStatistics(cache)
+ }
+
+ private fun assertStatistics(cache: LruCache<*, *>) {
+ assertEquals(expectedCreateCount, cache.createCount(), "create count")
+ assertEquals(expectedPutCount, cache.putCount(), "put count")
+ assertEquals(expectedHitCount, cache.hitCount(), "hit count")
+ assertEquals(expectedMissCount, cache.missCount(), "miss count")
+ assertEquals(expectedEvictionCount, cache.evictionCount(), "eviction count")
+ }
+
+ private fun <T> assertSnapshot(cache: LruCache<T, T>, vararg keysAndValues: T) {
+ val actualKeysAndValues = mutableListOf<T>()
+ for ((key, value) in cache.snapshot()) {
+ actualKeysAndValues.add(key)
+ actualKeysAndValues.add(value)
+ }
+ // assert using lists because order is important for LRUs
+ assertEquals(keysAndValues.asList(), actualKeysAndValues)
+ }
+}
\ No newline at end of file
diff --git a/collection/collection2/src/commonTest/kotlin/androidx/collection/SimpleArrayMapKotlinTest.kt b/collection/collection2/src/commonTest/kotlin/androidx/collection/SimpleArrayMapKotlinTest.kt
new file mode 100644
index 0000000..e2ce0b4
--- /dev/null
+++ b/collection/collection2/src/commonTest/kotlin/androidx/collection/SimpleArrayMapKotlinTest.kt
@@ -0,0 +1,332 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+class SimpleArrayMapKotlinTest {
+ @Test
+ fun equalsEmpty() {
+ val empty = SimpleArrayMap<String, String>()
+ assertTrue(empty.equals(empty))
+ assertTrue(empty.equals(emptyMap<Any, Any>()))
+ assertTrue(empty.equals(SimpleArrayMap<String, String>()))
+ assertTrue(empty.equals(hashMapOf<String, String>()))
+ assertFalse(empty.equals(mapOf(Pair("foo", "bar"))))
+ val simpleArrayMapNotEmpty = SimpleArrayMap<String, String>()
+ simpleArrayMapNotEmpty.put("foo", "bar")
+ assertFalse(empty.equals(simpleArrayMapNotEmpty))
+ val hashMapNotEquals = hashMapOf<String, String>()
+ hashMapNotEquals.put("foo", "bar")
+ assertFalse(empty.equals(hashMapNotEquals))
+ }
+
+ @Test
+ fun equalsNonEmpty() {
+ val map = SimpleArrayMap<String, String>()
+ map.put("foo", "bar")
+ assertTrue(map.equals(map))
+ assertTrue(map.equals(mapOf(Pair("foo", "bar"))))
+ val otherSimpleArrayMap = SimpleArrayMap<String, String>()
+ otherSimpleArrayMap.put("foo", "bar")
+ val otherHashMap = hashMapOf<String, String>()
+ otherHashMap.put("foo", "bar")
+ assertTrue(map.equals(otherHashMap))
+ assertFalse(map.equals(emptyMap<Any, Any>()))
+ assertFalse(map.equals(SimpleArrayMap<String, String>()))
+ assertFalse(map.equals(hashMapOf<String, String>()))
+ }
+
+ @Test
+ fun getOrDefaultPrefersStoredValue() {
+ val map = SimpleArrayMap<String, String>()
+ map.put("one", "1")
+ assertEquals("1", map.getOrDefault("one", "2"))
+ }
+
+ @Test
+ fun getOrDefaultUsesDefaultWhenAbsent() {
+ val map = SimpleArrayMap<String, String>()
+ assertEquals("1", map.getOrDefault("one", "1"))
+ }
+
+ @Test
+ fun getOrDefaultReturnsNullWhenNullStored() {
+ val map = SimpleArrayMap<String, String?>()
+ map.put("one", null)
+ assertNull(map.getOrDefault("one", "1"))
+ }
+
+ @Test
+ fun getOrDefaultDoesNotPersistDefault() {
+ val map = SimpleArrayMap<String, String>()
+ map.getOrDefault("one", "1")
+ assertFalse(map.containsKey("one"))
+ }
+
+ @Test
+ fun putIfAbsentDoesNotOverwriteStoredValue() {
+ val map = SimpleArrayMap<String, String>()
+ map.put("one", "1")
+ map.putIfAbsent("one", "2")
+ assertEquals("1", map.get("one"))
+ }
+
+ @Test
+ fun putIfAbsentReturnsStoredValue() {
+ val map = SimpleArrayMap<String, String>()
+ map.put("one", "1")
+ assertEquals("1", map.putIfAbsent("one", "2"))
+ }
+
+ @Test
+ fun putIfAbsentStoresValueWhenAbsent() {
+ val map = SimpleArrayMap<String, String>()
+ map.putIfAbsent("one", "2")
+ assertEquals("2", map.get("one"))
+ }
+
+ @Test
+ fun putIfAbsentReturnsNullWhenAbsent() {
+ val map = SimpleArrayMap<String, String>()
+ assertNull(map.putIfAbsent("one", "2"))
+ }
+
+ @Test
+ fun replaceWhenAbsentDoesNotStore() {
+ val map = SimpleArrayMap<String, String>()
+ assertNull(map.replace("one", "1"))
+ assertFalse(map.containsKey("one"))
+ }
+
+ @Test
+ fun replaceStoresAndReturnsOldValue() {
+ val map = SimpleArrayMap<String, String>()
+ map.put("one", "1")
+ assertEquals("1", map.replace("one", "2"))
+ assertEquals("2", map.get("one"))
+ }
+
+ @Test
+ fun replaceStoresAndReturnsNullWhenMappedToNull() {
+ val map = SimpleArrayMap<String, String?>()
+ map.put("one", null)
+ assertNull(map.replace("one", "1"))
+ assertEquals("1", map.get("one"))
+ }
+
+ @Test
+ fun replaceValueKeyAbsent() {
+ val map = SimpleArrayMap<String, String>()
+ assertFalse(map.replace("one", "1", "2"))
+ assertFalse(map.containsKey("one"))
+ }
+
+ @Test
+ fun replaceValueMismatchDoesNotReplace() {
+ val map = SimpleArrayMap<String, String>()
+ map.put("one", "1")
+ assertFalse(map.replace("one", "2", "3"))
+ assertEquals("1", map.get("one"))
+ }
+
+ @Test
+ fun replaceValueMismatchNullDoesNotReplace() {
+ val map = SimpleArrayMap<String, String?>()
+ map.put("one", "1")
+ assertFalse(map.replace("one", null, "2"))
+ assertEquals("1", map.get("one"))
+ }
+
+ @Test
+ fun replaceValueMatchReplaces() {
+ val map = SimpleArrayMap<String, String>()
+ map.put("one", "1")
+ assertTrue(map.replace("one", "1", "2"))
+ assertEquals("2", map.get("one"))
+ }
+
+ @Test
+ fun replaceNullValueMismatchDoesNotReplace() {
+ val map = SimpleArrayMap<String, String?>()
+ map.put("one", null)
+ assertFalse(map.replace("one", "1", "2"))
+ assertNull(map.get("one"))
+ }
+
+ @Test
+ fun replaceNullValueMatchRemoves() {
+ val map = SimpleArrayMap<String, String?>()
+ map.put("one", null)
+ assertTrue(map.replace("one", null, "1"))
+ assertEquals("1", map.get("one"))
+ }
+
+ @Test
+ fun removeValueKeyAbsent() {
+ val map = SimpleArrayMap<String, String>()
+ assertFalse(map.remove("one", "1"))
+ }
+
+ @Test
+ fun removeValueMismatchDoesNotRemove() {
+ val map = SimpleArrayMap<String, String>()
+ map.put("one", "1")
+ assertFalse(map.remove("one", "2"))
+ assertTrue(map.containsKey("one"))
+ }
+
+ @Test
+ fun removeValueMismatchNullDoesNotRemove() {
+ val map = SimpleArrayMap<String, String?>()
+ map.put("one", "1")
+ assertFalse(map.remove("one", null))
+ assertTrue(map.containsKey("one"))
+ }
+
+ @Test
+ fun removeValueMatchRemoves() {
+ val map = SimpleArrayMap<String, String>()
+ map.put("one", "1")
+ assertTrue(map.remove("one", "1"))
+ assertFalse(map.containsKey("one"))
+ }
+
+ @Test
+ fun removeNullValueMismatchDoesNotRemove() {
+ val map = SimpleArrayMap<String, String?>()
+ map.put("one", null)
+ assertFalse(map.remove("one", "2"))
+ assertTrue(map.containsKey("one"))
+ }
+
+ @Test
+ fun removeNullValueMatchRemoves() {
+ val map = SimpleArrayMap<String, String?>()
+ map.put("one", null)
+ assertTrue(map.remove("one", null))
+ assertFalse(map.containsKey("one"))
+ }
+
+ /**
+ * Check to make sure the same operations behave as expected in a single thread.
+ */
+ @Test
+ fun testNonConcurrentAccesses() {
+ val map = SimpleArrayMap<String, String>()
+ var i = 0
+ while (i < 100000) {
+ try {
+ map.put("key ${i++}", "B_DONT_DO_THAT")
+ if (i % 500 == 0) {
+ map.clear()
+ }
+ } catch (e: ConcurrentModificationException) {
+ println("Concurrent modification caught on single thread")
+ e.printStackTrace()
+ fail()
+ }
+ i++
+ }
+ }
+
+ /**
+ * Even though the Javadoc of [SimpleArrayMap.put] says that the key
+ * must not be null, the actual implementation allows it, and therefore we must ensure
+ * that any future implementations of the class will still honor that contract.
+ */
+ @Test
+ fun nullKeyCompatibility_canPutNullKeyAndNonNullValue() {
+ val map = SimpleArrayMap<String?, Int>()
+ assertFalse(map.containsKey(null))
+ map.put(null, 42)
+ assertTrue(map.containsKey(null))
+ }
+
+ @Test
+ fun nullKeyCompatibility_replacesValuesWithNullKey() {
+ val firstValue = 42
+ val secondValue = 43
+ val map = SimpleArrayMap<String?, Int>()
+ assertFalse(map.containsKey(null))
+ map.put(null, firstValue)
+ assertTrue(map.containsKey(null))
+ assertEquals(firstValue, map.get(null))
+ assertEquals(firstValue, map.put(null, secondValue))
+ assertEquals(secondValue, map.get(null))
+ assertEquals(secondValue, map.remove(null))
+ assertFalse(map.containsKey(null))
+ }
+
+ @Test
+ fun nullKeyCompatibility_putThenRemoveNullKeyAndValue() {
+ val map = SimpleArrayMap<String?, Int?>()
+ map.put(null, null)
+ assertTrue(map.containsKey(null))
+ assertNull(map.get(null))
+ map.remove(null)
+ assertFalse(map.containsKey(null))
+ }
+
+ @Test
+ fun nullKeyCompatibility_removeNonNullValueWithNullKey() {
+ val map = SimpleArrayMap<String?, String?>()
+ map.put(null, null)
+ assertNull(map.put(null, "42"))
+ assertEquals("42", map.get(null))
+ map.remove(null)
+ }
+
+ @Test
+ fun nullKeyCompatibility_testReplaceMethodsWithNullKey() {
+ val map = SimpleArrayMap<String?, String?>()
+ map.put(null, null)
+ assertNull(null, map.replace(null, "42"))
+ assertFalse(map.replace(null, null, null))
+ assertTrue(map.replace(null, "42", null))
+ assertFalse(map.replace(null, "42", null))
+ assertTrue(map.replace(null, null, null))
+ assertTrue(map.containsKey(null))
+ assertNull(map.get(null))
+ }
+
+ /**
+ * Regression test against NPE in changes in the backing array growth implementation. Various
+ * initial capacities are used, and for each capacity we always put in more elements than the
+ * initial capacity can hold to exercise the code paths where the capacity is increased and the
+ * backing arrays are expanded.
+ */
+ @Test
+ fun backingArrayGrowth() {
+ for (initCapacity in 0..16) {
+ for (entries in 1..31) {
+ val map = SimpleArrayMap<String, String>(initCapacity)
+ for (index in 0 until entries) {
+ map.put("key $index", "value $index")
+ }
+ for (index in 0 until entries) {
+ assertEquals("value $index", map.get("key $index"))
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/collection/collection2/src/commonTest/kotlin/androidx/collection/SparseArrayKotlinTest.kt b/collection/collection2/src/commonTest/kotlin/androidx/collection/SparseArrayKotlinTest.kt
new file mode 100644
index 0000000..499f475
--- /dev/null
+++ b/collection/collection2/src/commonTest/kotlin/androidx/collection/SparseArrayKotlinTest.kt
@@ -0,0 +1,283 @@
+/*
+ * 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.collection
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotSame
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+class SparseArrayKotlinTest {
+ @Test
+ fun getOrDefaultPrefersStoredValue() {
+ val map = SparseArray<String>()
+ map.put(1, "1")
+ assertEquals("1", map.get(1, "2"))
+ }
+
+ @Test
+ fun getOrDefaultUsesDefaultWhenAbsent() {
+ val map = SparseArray<String>()
+ assertEquals("1", map.get(1, "1"))
+ }
+
+ @Test
+ fun getOrDefaultReturnsNullWhenNullStored() {
+ val map = SparseArray<String?>()
+ map.put(1, null)
+ assertNull(map.get(1, "1"))
+ }
+
+ @Test
+ fun getOrDefaultDoesNotPersistDefault() {
+ val map = SparseArray<String>()
+ map.get(1, "1")
+ assertFalse(map.containsKey(1))
+ }
+
+ @Test
+ fun putIfAbsentDoesNotOverwriteStoredValue() {
+ val map = SparseArray<String>()
+ map.put(1, "1")
+ map.putIfAbsent(1, "2")
+ assertEquals("1", map.get(1))
+ }
+
+ @Test
+ fun putIfAbsentReturnsStoredValue() {
+ val map = SparseArray<String>()
+ map.put(1, "1")
+ assertEquals("1", map.putIfAbsent(1, "2"))
+ }
+
+ @Test
+ fun putIfAbsentStoresValueWhenAbsent() {
+ val map = SparseArray<String>()
+ map.putIfAbsent(1, "2")
+ assertEquals("2", map.get(1))
+ }
+
+ @Test
+ fun putIfAbsentReturnsNullWhenAbsent() {
+ val map = SparseArray<String>()
+ assertNull(map.putIfAbsent(1, "2"))
+ }
+
+ @Test
+ fun replaceWhenAbsentDoesNotStore() {
+ val map = SparseArray<String>()
+ assertNull(map.replace(1, "1"))
+ assertFalse(map.containsKey(1))
+ }
+
+ @Test
+ fun replaceStoresAndReturnsOldValue() {
+ val map = SparseArray<String>()
+ map.put(1, "1")
+ assertEquals("1", map.replace(1, "2"))
+ assertEquals("2", map.get(1))
+ }
+
+ @Test
+ fun replaceStoresAndReturnsNullWhenMappedToNull() {
+ val map = SparseArray<String?>()
+ map.put(1, null)
+ assertNull(map.replace(1, "1"))
+ assertEquals("1", map.get(1))
+ }
+
+ @Test
+ fun replaceValueKeyAbsent() {
+ val map = SparseArray<String>()
+ assertFalse(map.replace(1, "1", "2"))
+ assertFalse(map.containsKey(1))
+ }
+
+ @Test
+ fun replaceValueMismatchDoesNotReplace() {
+ val map = SparseArray<String>()
+ map.put(1, "1")
+ assertFalse(map.replace(1, "2", "3"))
+ assertEquals("1", map.get(1))
+ }
+
+ @Test
+ fun replaceValueMismatchNullDoesNotReplace() {
+ val map = SparseArray<String>()
+ map.put(1, "1")
+ assertFalse(map.replace(1, null, "2"))
+ assertEquals("1", map.get(1))
+ }
+
+ @Test
+ fun replaceValueMatchReplaces() {
+ val map = SparseArray<String>()
+ map.put(1, "1")
+ assertTrue(map.replace(1, "1", "2"))
+ assertEquals("2", map.get(1))
+ }
+
+ @Test
+ fun replaceNullValueMismatchDoesNotReplace() {
+ val map = SparseArray<String?>()
+ map.put(1, null)
+ assertFalse(map.replace(1, "1", "2"))
+ assertNull(map.get(1))
+ }
+
+ @Test
+ fun replaceNullValueMatchRemoves() {
+ val map = SparseArray<String?>()
+ map.put(1, null)
+ assertTrue(map.replace(1, null, "1"))
+ assertEquals("1", map.get(1))
+ }
+
+ @Test
+ fun removeValueKeyAbsent() {
+ val map = SparseArray<String>()
+ assertFalse(map.remove(1, "1"))
+ }
+
+ @Test
+ fun removeValueMismatchDoesNotRemove() {
+ val map = SparseArray<String>()
+ map.put(1, "1")
+ assertFalse(map.remove(1, "2"))
+ assertTrue(map.containsKey(1))
+ }
+
+ @Test
+ fun removeValueMismatchNullDoesNotRemove() {
+ val map = SparseArray<String>()
+ map.put(1, "1")
+ assertFalse(map.remove(1, null))
+ assertTrue(map.containsKey(1))
+ }
+
+ @Test
+ fun removeValueMatchRemoves() {
+ val map = SparseArray<String>()
+ map.put(1, "1")
+ assertTrue(map.remove(1, "1"))
+ assertFalse(map.containsKey(1))
+ }
+
+ @Test
+ fun removeNullValueMismatchDoesNotRemove() {
+ val map = SparseArray<String?>()
+ map.put(1, null)
+ assertFalse(map.remove(1, "2"))
+ assertTrue(map.containsKey(1))
+ }
+
+ @Test
+ fun removeNullValueMatchRemoves() {
+ val map = SparseArray<String?>()
+ map.put(1, null)
+ assertTrue(map.remove(1, null))
+ assertFalse(map.containsKey(1))
+ }
+
+ @Test
+ fun isEmpty() {
+ val array = SparseArray<String>()
+ assertTrue(array.isEmpty()) // Newly created SparseArrayCompat should be empty
+
+ // Adding elements should change state from empty to not empty.
+ for (i in 0..4) {
+ array.put(i, i.toString())
+ assertFalse(array.isEmpty())
+ }
+ array.clear()
+ assertTrue(array.isEmpty()) // A cleared SparseArrayCompat should be empty.
+ val key1 = 1
+ val key2 = 2
+ val value1 = "some value"
+ val value2 = "some other value"
+ array.append(key1, value1)
+ assertFalse(array.isEmpty()) // has 1 element.
+ array.append(key2, value2)
+ assertFalse(array.isEmpty()) // has 2 elements.
+ assertFalse(array.isEmpty()) // consecutive calls should be OK.
+ array.remove(key1)
+ assertFalse(array.isEmpty()) // has 1 element.
+ array.remove(key2)
+ assertTrue(array.isEmpty())
+ }
+
+ @Test
+ fun containsKey() {
+ val array = SparseArray<String>()
+ array.put(1, "one")
+ assertTrue(array.containsKey(1))
+ assertFalse(array.containsKey(2))
+ }
+
+ @Test
+ fun containsValue() {
+ val array = SparseArray<String>()
+ array.put(1, "one")
+ assertTrue(array.containsValue("one"))
+ assertFalse(array.containsValue("two"))
+ }
+
+ @Test
+ fun putAll() {
+ val dest = SparseArray<String>()
+ dest.put(1, "one")
+ dest.put(3, "three")
+ val source = SparseArray<String>()
+ source.put(1, "uno")
+ source.put(2, "dos")
+ dest.putAll(source)
+ assertEquals(3, dest.size.toLong())
+ assertEquals("uno", dest.get(1))
+ assertEquals("dos", dest.get(2))
+ assertEquals("three", dest.get(3))
+ }
+
+ @Test
+ fun putAllVariance() {
+ val dest = SparseArray<Any>()
+ dest.put(1, 1L)
+ val source = SparseArray<String>()
+ dest.put(2, "two")
+ dest.putAll(source)
+ assertEquals(2, dest.size.toLong())
+ assertEquals(1L, dest.get(1))
+ assertEquals("two", dest.get(2))
+ }
+
+ @Test
+ fun copyConstructor() {
+ val source = SparseArray<String>()
+ source.put(42, "cuarenta y dos")
+ source.put(3, "tres")
+ source.put(11, "once")
+ val dest = SparseArray(source)
+ assertNotSame(source, dest)
+
+ assertEquals(source.size, dest.size)
+ for (i in 0 until source.size) {
+ assertEquals(source.keyAt(i), dest.keyAt(i))
+ assertEquals(source.valueAt(i), dest.valueAt(i))
+ }
+ }
+}
\ No newline at end of file
diff --git a/collection/collection2/src/commonTest/kotlin/androidx/collection/testing.kt b/collection/collection2/src/commonTest/kotlin/androidx/collection/testing.kt
new file mode 100644
index 0000000..0fe990a0
--- /dev/null
+++ b/collection/collection2/src/commonTest/kotlin/androidx/collection/testing.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("TestingCommonKt")
+
+package androidx.collection
+
+import kotlin.jvm.JvmName
+
+inline fun <reified T> assertThrows(body: () -> Any) {
+ var fail = true
+ try {
+ body()
+ fail = false
+ } catch (t: Throwable) {
+ if (t !is T) {
+ throw t
+ }
+ }
+ if (!fail) {
+ throw AssertionError("Expected ${T::class} but body completed successfully")
+ }
+}
+
+expect fun testBody(body: () -> Unit)
diff --git a/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt b/collection/collection2/src/jsTest/kotlin/androidx/collection/testing.kt
similarity index 80%
copy from benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt
copy to collection/collection2/src/jsTest/kotlin/androidx/collection/testing.kt
index 88234c7..6ebf781 100644
--- a/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt
+++ b/collection/collection2/src/jsTest/kotlin/androidx/collection/testing.kt
@@ -14,10 +14,6 @@
* limitations under the License.
*/
-package androidx.benchmark.macro
+package androidx.collection
-import android.platform.test.rule.DropCachesRule
-
-class BenchmarkClass {
- val rule = DropCachesRule()
-}
+actual inline fun testBody(body: () -> Unit) = body()
diff --git a/collection/collection2/src/jvmMain/kotlin/androidx/collection/LongSparseArray.kt b/collection/collection2/src/jvmMain/kotlin/androidx/collection/LongSparseArray.kt
new file mode 100644
index 0000000..c50404d
--- /dev/null
+++ b/collection/collection2/src/jvmMain/kotlin/androidx/collection/LongSparseArray.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.collection
+
+import kotlin.DeprecationLevel.HIDDEN
+
+actual open class LongSparseArray<E>
+@JvmOverloads actual constructor(initialCapacity: Int) : Cloneable {
+ @JvmField
+ @JvmSynthetic // Hide from Java callers.
+ internal actual var keys: LongArray
+ @JvmField
+ @JvmSynthetic // Hide from Java callers.
+ internal actual var values: Array<Any?>
+ init {
+ if (initialCapacity == 0) {
+ keys = EMPTY_LONGS
+ values = EMPTY_OBJECTS
+ } else {
+ val length = idealLongArraySize(initialCapacity)
+ keys = LongArray(length)
+ values = arrayOfNulls(length)
+ }
+ }
+
+ @JvmField
+ @JvmSynthetic // Hide from Java callers.
+ internal actual var garbage: Boolean = false
+ @JvmField
+ @JvmSynthetic // Hide from Java callers.
+ @Suppress("PropertyName") // Normal backing field name but internal for common code.
+ internal actual var _size: Int = 0
+
+ actual constructor(array: LongSparseArray<E>) : this(0) {
+ _size = array._size
+ keys = array.keys.copyOf()
+ values = array.values.copyOf()
+ garbage = array.garbage
+ gc()
+ }
+
+ // Suppression necessary, see KT-43542.
+ @Suppress("INAPPLICABLE_JVM_NAME")
+ @get:JvmName("size")
+ actual open val size: Int get() = commonSize()
+
+ actual open fun isEmpty(): Boolean = commonIsEmpty()
+
+ actual open operator fun get(key: Long): E? = commonGet(key, null)
+ actual open fun get(key: Long, default: E): E = commonGet(key, default)
+
+ actual open fun put(key: Long, value: E): Unit = commonPut(key, value)
+ actual open fun putAll(other: LongSparseArray<out E>): Unit = commonPutAll(other)
+ actual open fun putIfAbsent(key: Long, value: E): E? = commonPutIfAbsent(key, value)
+ actual open fun append(key: Long, value: E): Unit = commonAppend(key, value)
+
+ actual open fun keyAt(index: Int): Long = commonKeyAt(index)
+ actual open fun valueAt(index: Int): E = commonValueAt(index)
+ actual open fun setValueAt(index: Int, value: E): Unit = commonSetValueAt(index, value)
+
+ actual open fun indexOfKey(key: Long): Int = commonIndexOfKey(key)
+ actual open fun indexOfValue(value: E): Int = commonIndexOfValue(value)
+
+ actual open fun containsKey(key: Long): Boolean = commonContainsKey(key)
+ actual open fun containsValue(value: E): Boolean = commonContainsValue(value)
+
+ actual open fun clear(): Unit = commonClear()
+
+ actual open fun remove(key: Long): Unit = commonRemove(key)
+
+ @Deprecated("", level = HIDDEN) // For binary compatibility.
+ open fun delete(key: Long): Unit = remove(key)
+
+ actual open fun remove(key: Long, value: Any?): Boolean = commonRemove(key, value)
+ actual open fun removeAt(index: Int): Unit = commonRemoveAt(index)
+
+ actual open fun replace(key: Long, value: E): E? = commonReplace(key, value)
+ actual open fun replace(key: Long, oldValue: E?, newValue: E): Boolean =
+ commonReplace(key, oldValue, newValue)
+
+ @Suppress("NoClone") // To suppress Metalava prohibition on cloning.
+ public override fun clone(): LongSparseArray<E> {
+ @Suppress("UNCHECKED_CAST") // Safe according to cloneable contract.
+ val clone = super.clone() as LongSparseArray<E>
+ clone.keys = keys.copyOf()
+ clone.values = values.copyOf()
+ return clone
+ }
+
+ override fun toString(): String = commonToString()
+}
diff --git a/collection/collection2/src/jvmMain/kotlin/androidx/collection/Platform.kt b/collection/collection2/src/jvmMain/kotlin/androidx/collection/Platform.kt
new file mode 100644
index 0000000..cac6670
--- /dev/null
+++ b/collection/collection2/src/jvmMain/kotlin/androidx/collection/Platform.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.collection
+
+internal actual fun indexOutOfBounds(): IndexOutOfBoundsException {
+ // Throw AIOOB on JVM for behavioral compatibility with 1.x.
+ return ArrayIndexOutOfBoundsException()
+}
+
+/**
+ * A class for synchronization monitors. Note: do not make this a subclass of any other class from
+ * a non-native synchronization library (such as AtomicFU). It's not needed on JVM/Android, and past
+ * experience showed some difficulties with effective dead-code elimination with R8.
+ */
+internal actual class SynchronizedObject
+
+internal actual fun createSynchronizedObject() = SynchronizedObject()
+
+internal actual inline fun <R> synchronizedOperation(lock: SynchronizedObject, block: () -> R): R {
+ synchronized(lock) {
+ return block()
+ }
+}
diff --git a/collection/collection2/src/jvmMain/kotlin/androidx/collection/SourceCompatibility.kt b/collection/collection2/src/jvmMain/kotlin/androidx/collection/SourceCompatibility.kt
new file mode 100644
index 0000000..a43a025
--- /dev/null
+++ b/collection/collection2/src/jvmMain/kotlin/androidx/collection/SourceCompatibility.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// For Kotlin source compatibility only.
+@file:JvmName("SourceCompatibility_DoNotUseFromJava")
+
+package androidx.collection
+
+import kotlin.DeprecationLevel.WARNING
+
+@Deprecated("Replaced with property", ReplaceWith("this.size"), WARNING)
+fun <K, V> ArrayMap<K, V>.size() = size
+
+// Note that there is no extension property ArrayMap<K, V>.isEmpty in JVM. Since ArrayMap is still
+// implemented in Java, Kotlin will allow isEmpty to be used both as a function and a property, as
+// is the case for any Java boolean isX() getter.
+
+@Deprecated("Replaced with property", ReplaceWith("this.size"), WARNING)
+fun <E> ArraySet<E>.size() = size
+
+@Deprecated("Replaced with function", ReplaceWith("this.isEmpty()"), WARNING)
+val <E> ArraySet<E>.isEmpty get() = isEmpty()
+
+@Deprecated("Replaced with property", ReplaceWith("this.size"), WARNING)
+fun <E> CircularArray<E>.size() = size
+
+@Deprecated("Replaced with function", ReplaceWith("this.isEmpty()"), WARNING)
+val <E> CircularArray<E>.isEmpty get() = isEmpty()
+
+@Deprecated("Replaced with property", ReplaceWith("this.size"), WARNING)
+fun CircularIntArray.size() = size
+
+@Deprecated("Replaced with function", ReplaceWith("this.isEmpty()"), WARNING)
+val CircularIntArray.isEmpty get() = isEmpty()
+
+@Deprecated("Replaced with property", ReplaceWith("this.size"), WARNING)
+fun <E> LongSparseArray<E>.size() = size
+
+@Deprecated("Replaced with function", ReplaceWith("this.isEmpty()"), WARNING)
+val <E> LongSparseArray<E>.isEmpty get() = isEmpty()
+
+@Deprecated("Replaced with property", ReplaceWith("this.size"), WARNING)
+fun <K, V> LruCache<K, V>.size() = size
+
+@Deprecated("Replaced with property", ReplaceWith("this.size"), WARNING)
+fun <K, V> SimpleArrayMap<K, V>.size() = size
+
+@Deprecated("Replaced with function", ReplaceWith("this.isEmpty()"), WARNING)
+val <K, V> SimpleArrayMap<K, V>.isEmpty get() = isEmpty()
+
+@Deprecated("Replaced with property", ReplaceWith("this.size"), WARNING)
+fun <E> SparseArray<E>.size() = size
+
+@Deprecated("Replaced with function", ReplaceWith("this.isEmpty()"), WARNING)
+val <E> SparseArray<E>.isEmpty get() = isEmpty()
+
+@Deprecated("Replaced with property", ReplaceWith("this.size"), WARNING)
+@Suppress("DEPRECATION") // Don't warn against extension on the deprecated class
+fun <E> SparseArrayCompat<E>.size() = size
+
+@Deprecated("Replaced with function", ReplaceWith("this.isEmpty()"), WARNING)
+@Suppress("DEPRECATION")
+val <E> SparseArrayCompat<E>.isEmpty get() = isEmpty()
diff --git a/collection/collection2/src/jvmMain/kotlin/androidx/collection/SparseArray.kt b/collection/collection2/src/jvmMain/kotlin/androidx/collection/SparseArray.kt
new file mode 100644
index 0000000..4c06669
--- /dev/null
+++ b/collection/collection2/src/jvmMain/kotlin/androidx/collection/SparseArray.kt
@@ -0,0 +1,135 @@
+/*
+ * 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.collection
+
+import kotlin.DeprecationLevel.HIDDEN
+
+actual open class SparseArray<E>
+@JvmOverloads actual constructor(initialCapacity: Int) : Cloneable {
+ @JvmField
+ @JvmSynthetic // Hide from Java callers.
+ internal actual var keys: IntArray
+ @JvmField
+ @JvmSynthetic // Hide from Java callers.
+ internal actual var values: Array<Any?>
+ init {
+ if (initialCapacity == 0) {
+ keys = EMPTY_INTS
+ values = EMPTY_OBJECTS
+ } else {
+ val length = idealIntArraySize(initialCapacity)
+ keys = IntArray(length)
+ values = arrayOfNulls(length)
+ }
+ }
+
+ @JvmField
+ @JvmSynthetic // Hide from Java callers.
+ internal actual var garbage: Boolean = false
+ @JvmField
+ @JvmSynthetic // Hide from Java callers.
+ @Suppress("PropertyName") // Normal backing field name but internal for common code.
+ internal actual var _size: Int = 0
+
+ actual constructor(array: SparseArray<E>) : this(0) {
+ _size = array._size
+ keys = array.keys.copyOf()
+ values = array.values.copyOf()
+ garbage = array.garbage
+ gc()
+ }
+
+ // Suppression necessary, see KT-43542.
+ @Suppress("INAPPLICABLE_JVM_NAME")
+ @get:JvmName("size") // Binary compatibility with Java.
+ actual open val size: Int get() = commonSize()
+
+ actual fun isEmpty(): Boolean = commonIsEmpty()
+
+ actual operator fun get(key: Int): E? = commonGet(key, null)
+ actual fun get(key: Int, default: E): E = commonGet(key, default)
+
+ actual fun put(key: Int, value: E): Unit = commonPut(key, value)
+ actual fun putAll(other: SparseArray<out E>): Unit = commonPutAll(other)
+ actual fun putIfAbsent(key: Int, value: E): E? = commonPutIfAbsent(key, value)
+ actual fun append(key: Int, value: E): Unit = commonAppend(key, value)
+
+ actual fun keyAt(index: Int): Int = commonKeyAt(index)
+
+ actual fun valueAt(index: Int): E = commonValueAt(index)
+ actual fun setValueAt(index: Int, value: E): Unit = commonSetValueAt(index, value)
+
+ actual fun indexOfKey(key: Int): Int = commonIndexOfKey(key)
+ actual fun indexOfValue(value: E): Int = commonIndexOfValue(value)
+
+ actual fun containsKey(key: Int): Boolean = commonContainsKey(key)
+ actual fun containsValue(value: E): Boolean = commonContainsValue(value)
+
+ actual fun clear(): Unit = commonClear()
+
+ actual fun remove(key: Int): Unit = commonRemove(key)
+ actual fun remove(key: Int, value: Any?): Boolean = commonRemove(key, value)
+ actual fun removeAt(index: Int): Unit = commonRemoveAt(index)
+
+ actual fun replace(key: Int, value: E): E? = commonReplace(key, value)
+ actual fun replace(key: Int, oldValue: E?, newValue: E): Boolean =
+ commonReplace(key, oldValue, newValue)
+
+ @Suppress("NoClone") // To suppress Metalava prohibition on cloning.
+ public override fun clone(): SparseArray<E> {
+ @Suppress("UNCHECKED_CAST") // Safe according to cloneable contract.
+ val clone = super.clone() as SparseArray<E>
+ clone.keys = keys.copyOf()
+ clone.values = values.copyOf()
+ return clone
+ }
+
+ override fun toString(): String = commonToString()
+
+ @Deprecated("Use remove(key)", level = HIDDEN) // For Java binary compatibility.
+ fun delete(key: Int): Unit = remove(key)
+
+ @Deprecated("Use ???", level = HIDDEN) // For Java binary compatibility.
+ fun removeAtRange(index: Int, count: Int) {
+ val end = minOf(_size, index + count)
+ for (i in index until end) {
+ removeAt(i)
+ }
+ }
+}
+
+// typealias can't be used for JVM interop, so we have to resort to this.
+// TODO(KT-21489): Follow up.
+@Deprecated("Use SparseArray")
+open class SparseArrayCompat<E> : SparseArray<E> {
+ @Deprecated("Use SparseArray", ReplaceWith("SparseArray<E>()"))
+ constructor() : super()
+ @Deprecated("Use SparseArray", ReplaceWith("SparseArray<E>(initialCapacity)"))
+ constructor(initialCapacity: Int) : super(initialCapacity)
+ @Deprecated("Use SparseArray", ReplaceWith("SparseArray<E>(array)"))
+ constructor(array: SparseArray<E>) : super(array)
+
+ @Suppress("DEPRECATION", "NoClone") // For compatibility.
+ override fun clone(): SparseArrayCompat<E> {
+ return super.clone() as SparseArrayCompat<E>
+ }
+
+ @Suppress("DEPRECATION") // For compatibility.
+ open fun putAll(other: SparseArrayCompat<out E>) {
+ super.putAll(other)
+ }
+}
diff --git a/collection/collection2/src/jvmTest/kotlin/androidx/collection/ArrayMapThreadedTest.kt b/collection/collection2/src/jvmTest/kotlin/androidx/collection/ArrayMapThreadedTest.kt
new file mode 100644
index 0000000..16dd083
--- /dev/null
+++ b/collection/collection2/src/jvmTest/kotlin/androidx/collection/ArrayMapThreadedTest.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.collection
+
+import org.junit.Test
+import java.util.ConcurrentModificationException
+import kotlin.test.fail
+
+class ArrayMapThreadedTest {
+ var map: ArrayMap<String, String>? = ArrayMap()
+
+ /**
+ * Attempt to generate a ConcurrentModificationException in ArrayMap.
+ *
+ * ArrayMap is explicitly documented to be non-thread-safe, yet it's easy to accidentally screw
+ * this up; ArrayMap should (in the spirit of the core Java collection types) make an effort to
+ * catch this and throw ConcurrentModificationException instead of crashing somewhere in its
+ * internals.
+ */
+ @Test
+ fun testConcurrentModificationException() {
+ val testLenMs = 5000
+ Thread {
+ var i = 0
+ while (map != null) {
+ try {
+ map?.put("key $i", "B_DONT_DO_THAT")
+ } catch (e: ArrayIndexOutOfBoundsException) {
+ fail(cause = e)
+ } catch (e: ClassCastException) {
+ fail(cause = e)
+ } catch (e: ConcurrentModificationException) {
+ println("[successfully caught CME at put #$i, size=${map?.size}]")
+ }
+ }
+ }.start()
+ for (i in 0 until testLenMs / 100) {
+ try {
+ Thread.sleep(100)
+ map?.clear()
+ } catch (e: InterruptedException) {
+ } catch (e: ArrayIndexOutOfBoundsException) {
+ fail(cause = e)
+ } catch (e: ClassCastException) {
+ fail(cause = e)
+ } catch (e: ConcurrentModificationException) {
+ println("[successfully caught CME at clear #$i size=${map?.size}]")
+ }
+ }
+ map = null // will stop other thread
+ }
+}
\ No newline at end of file
diff --git a/collection/collection2/src/jvmTest/kotlin/androidx/collection/ArraySetThreadedTest.kt b/collection/collection2/src/jvmTest/kotlin/androidx/collection/ArraySetThreadedTest.kt
new file mode 100644
index 0000000..d94fa88
--- /dev/null
+++ b/collection/collection2/src/jvmTest/kotlin/androidx/collection/ArraySetThreadedTest.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.collection
+
+import org.junit.Test
+import java.util.ConcurrentModificationException
+import kotlin.test.AfterTest
+import kotlin.test.fail
+
+class ArraySetThreadedTest {
+ var set: ArraySet<String>? = ArraySet()
+
+ @AfterTest
+ fun cleanUp() {
+ set = null
+ }
+
+ @Test
+ fun testConcurrentModificationException() {
+ val testDurMs = 10000
+ println("Starting ArraySet concurrency test")
+ Thread {
+ var i = 0
+ val s = set
+ while (s != null) {
+ try {
+ s.add(String.format("key %d", i++))
+ } catch (e: ArrayIndexOutOfBoundsException) {
+ fail(cause = e)
+ } catch (e: ClassCastException) {
+ fail(cause = e)
+ } catch (e: ConcurrentModificationException) {
+ println(
+ "[successfully caught CME at put #$i size=${s.size}]"
+ )
+ }
+ }
+ }.start()
+ for (i in 0 until testDurMs / 100) {
+ try {
+ if (set != null && set!!.size % 4 == 0) {
+ set?.clear()
+ }
+ } catch (e: ArrayIndexOutOfBoundsException) {
+ fail(e.message)
+ } catch (e: ClassCastException) {
+ fail(e.message)
+ } catch (e: ConcurrentModificationException) {
+ println(
+ "[successfully caught CME at clear #$i size=${set?.size}]"
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/collection/collection2/src/jvmTest/kotlin/androidx/collection/CloneableTest.kt b/collection/collection2/src/jvmTest/kotlin/androidx/collection/CloneableTest.kt
new file mode 100644
index 0000000..d058a5d
--- /dev/null
+++ b/collection/collection2/src/jvmTest/kotlin/androidx/collection/CloneableTest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.collection
+
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotSame
+
+class CloneableTest {
+ @Test
+ fun longSparseArrayCloneTest() {
+ val source = LongSparseArray<String>()
+ source.put(42, "cuarenta y dos")
+ source.put(3, "tres")
+ source.put(11, "once")
+ val dest = source.clone()
+ assertNotSame(source, dest)
+
+ assertEquals(source.size, dest.size)
+ for (i in 0 until source.size) {
+ assertEquals(source.keyAt(i), dest.keyAt(i))
+ assertEquals(source.valueAt(i), dest.valueAt(i))
+ }
+ }
+
+ @Test
+ fun sparseArrayCloneTest() {
+ val source = SparseArray<String>()
+ source.put(42, "cuarenta y dos")
+ source.put(3, "tres")
+ source.put(11, "once")
+ val dest = source.clone()
+ assertNotSame(source, dest)
+
+ assertEquals(source.size, dest.size)
+ for (i in 0 until source.size) {
+ assertEquals(source.keyAt(i), dest.keyAt(i))
+ assertEquals(source.valueAt(i), dest.valueAt(i))
+ }
+ }
+
+ @Suppress("DEPRECATION") // For SpareArrayCompat usage
+ @Test
+ fun sparseArrayCompatCloneTest() {
+ val source = SparseArrayCompat<String>()
+ source.put(42, "cuarenta y dos")
+ source.put(3, "tres")
+ source.put(11, "once")
+ val dest = source.clone()
+ assertNotSame(source, dest)
+
+ assertEquals(source.size, dest.size)
+ for (i in 0 until source.size) {
+ assertEquals(source.keyAt(i), dest.keyAt(i))
+ assertEquals(source.valueAt(i), dest.valueAt(i))
+ }
+ }
+}
\ No newline at end of file
diff --git a/collection/collection2/src/jvmTest/kotlin/androidx/collection/LruCacheThreadedTest.kt b/collection/collection2/src/jvmTest/kotlin/androidx/collection/LruCacheThreadedTest.kt
new file mode 100644
index 0000000..508b29d
--- /dev/null
+++ b/collection/collection2/src/jvmTest/kotlin/androidx/collection/LruCacheThreadedTest.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.collection
+
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.fail
+
+class LruCacheThreadedTest {
+ @Test
+ fun testEntryRemovedIsCalledWithoutSynchronization() {
+ val cache = object : LruCache<String, String>(3) {
+ override fun entryRemoved(
+ evicted: Boolean,
+ key: String,
+ oldValue: String,
+ newValue: String?
+ ) {
+ assertFalse(Thread.holdsLock(this))
+ }
+ }
+ cache.put("a", "A")
+ cache.put("a", "A2") // replaced
+ cache.put("b", "B")
+ cache.put("c", "C")
+ cache.put("d", "D") // single eviction
+ cache.remove("a") // removed
+ cache.evictAll() // multiple eviction
+ }
+
+ /** Makes sure that LruCache operations are correctly synchronized to guarantee consistency. */
+ @Test
+ fun consistentMultithreadedAccess() {
+ class Tally {
+ var mNonNullValues = 0
+ var mNullValues = 0
+ var mValuesPut = 0
+ var mConflicts = 0
+ var mRemoved = 0
+ }
+
+ val tally = Tally()
+ val rounds = 10000
+ val key = "key"
+ val value = 42
+ val cache = object : LruCache<String, Int?>(1) {
+ override fun create(key: String): Int? {
+ return value
+ }
+ }
+
+ val r0 = Runnable {
+ for (i in 0 until rounds) {
+ if (cache.get(key) != null) {
+ tally.mNonNullValues++
+ } else {
+ tally.mNullValues++
+ }
+ }
+ }
+ val r1 = Runnable {
+ for (i in 0 until rounds) {
+ if (i % 2 == 0) {
+ if (cache.put(key, value) != null) {
+ tally.mConflicts++
+ } else {
+ tally.mValuesPut++
+ }
+ } else {
+ cache.remove(key)
+ tally.mRemoved++
+ }
+ }
+ }
+
+ val t0: Thread = Thread(r0)
+ val t1: Thread = Thread(r1)
+ t0.start()
+ t1.start()
+ try {
+ t0.join()
+ t1.join()
+ } catch (e: InterruptedException) {
+ fail()
+ }
+ assertEquals(rounds.toLong(), tally.mNonNullValues.toLong())
+ assertEquals(0, tally.mNullValues.toLong())
+ assertEquals(
+ rounds.toLong(),
+ tally.mValuesPut + tally.mConflicts + tally.mRemoved.toLong()
+ )
+ }
+}
\ No newline at end of file
diff --git a/collection/collection2/src/jvmTest/kotlin/androidx/collection/SimpleArrayMapThreadedTest.kt b/collection/collection2/src/jvmTest/kotlin/androidx/collection/SimpleArrayMapThreadedTest.kt
new file mode 100644
index 0000000..470ca6b
--- /dev/null
+++ b/collection/collection2/src/jvmTest/kotlin/androidx/collection/SimpleArrayMapThreadedTest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.collection
+
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.test.Test
+import kotlin.test.fail
+
+class SimpleArrayMapThreadedTest {
+ /**
+ * Attempt to generate a ConcurrentModificationException in ArrayMap.
+ */
+ @Test
+ fun testConcurrentModificationException() {
+ val map = SimpleArrayMap<String, String>()
+ val done = AtomicBoolean()
+ val TEST_LEN_MS = 5000
+ println("Starting SimpleArrayMap concurrency test")
+ Thread(
+ Runnable() {
+ fun run() {
+ var i = 0
+ while (!done.get()) {
+ try {
+ map.put("key ${i++}", "B_DONT_DO_THAT")
+ } catch (e: ArrayIndexOutOfBoundsException) {
+ // SimpleArrayMap is not thread safe, so lots of concurrent modifications
+ // can still cause data corruption
+ println("concurrent modification uncaught, causing indexing failure")
+ e.printStackTrace()
+ } catch (e: ClassCastException) {
+ // cache corruption should not occur as it is hard to trace and one thread
+ // may corrupt the pool for all threads in the same process.
+ fail("concurrent modification uncaught, causing cache corruption", e)
+ } catch (e: ConcurrentModificationException) {
+ // Ignored.
+ }
+ }
+ }
+ }
+ ).start()
+
+ for (i in 0 until TEST_LEN_MS / 100) {
+ try {
+ Thread.sleep(100)
+ map.clear()
+ } catch (e: InterruptedException) {
+ } catch (e: ArrayIndexOutOfBoundsException) {
+ fail("concurrent modification uncaught, causing indexing failure", e)
+ } catch (e: ClassCastException) {
+ fail("concurrent modification uncaught, causing cache corruption", e)
+ } catch (e: ConcurrentModificationException) {
+ // Ignored.
+ }
+ }
+ done.set(true)
+ }
+}
\ No newline at end of file
diff --git a/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt b/collection/collection2/src/jvmTest/kotlin/androidx/collection/testing.kt
similarity index 81%
rename from benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt
rename to collection/collection2/src/jvmTest/kotlin/androidx/collection/testing.kt
index 88234c7..47620a0 100644
--- a/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt
+++ b/collection/collection2/src/jvmTest/kotlin/androidx/collection/testing.kt
@@ -14,10 +14,8 @@
* limitations under the License.
*/
-package androidx.benchmark.macro
+package androidx.collection
-import android.platform.test.rule.DropCachesRule
-
-class BenchmarkClass {
- val rule = DropCachesRule()
+actual fun testBody(body: () -> Unit) {
+ body()
}
diff --git a/collection/collection2/src/nativeTest/kotlin/androidx/collection/testing.kt b/collection/collection2/src/nativeTest/kotlin/androidx/collection/testing.kt
new file mode 100644
index 0000000..9f2fc75
--- /dev/null
+++ b/collection/collection2/src/nativeTest/kotlin/androidx/collection/testing.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.collection
+
+import kotlin.native.concurrent.TransferMode.SAFE
+import kotlin.native.concurrent.Worker
+import kotlin.native.concurrent.freeze
+
+actual fun testBody(body: () -> Unit) {
+ // Run the test body twice, once on main and once on a worker. These are encapsulated in
+ // functions so that the stacktrace tells you which failed.
+ runOnMainThread(body)
+ runOnWorkerThread(body)
+}
+
+private fun runOnMainThread(body: () -> Unit) {
+ body()
+}
+
+private fun runOnWorkerThread(body: () -> Unit) {
+ body.freeze()
+
+ val worker = Worker.start()
+ val future = worker.execute(SAFE, { body }) {
+ runCatching(it)
+ }
+ future.result.getOrThrow()
+}
diff --git a/collection/collection2/src/nonJvmMain/kotlin/androidx/collection/ArrayMap.kt b/collection/collection2/src/nonJvmMain/kotlin/androidx/collection/ArrayMap.kt
new file mode 100644
index 0000000..68d3605
--- /dev/null
+++ b/collection/collection2/src/nonJvmMain/kotlin/androidx/collection/ArrayMap.kt
@@ -0,0 +1,149 @@
+/*
+ * 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.collection
+
+open class ArrayMap<K, V> : SimpleArrayMap<K, V>, MutableMap<K, V> {
+
+ constructor(capacity: Int = 0) : super(capacity)
+
+ constructor(map: SimpleArrayMap<K, V>) : super(map)
+
+ /**
+ * Determine if the array map contains all of the keys in the given collection.
+ * @param collection The collection whose contents are to be checked against.
+ * @return Returns true if this array map contains a key for every entry
+ * in <var>collection</var>, else returns false.
+ */
+ open fun containsAll(collection: Collection<K>): Boolean {
+ for (o in collection) {
+ if (!containsKey(o)) {
+ return false
+ }
+ }
+ return true
+ }
+
+ /**
+ * Perform a {@link #put(Object, Object)} of all key/value pairs in <var>map</var>
+ * @param from The map whose contents are to be retrieved.
+ */
+ override fun putAll(from: Map<out K, V>) {
+ ensureCapacity(_size + from.size)
+ for ((key, value) in from.entries) {
+ put(key, value)
+ }
+ }
+
+ /**
+ * Remove all keys in the array map that exist in the given collection.
+ * @param collection The collection whose contents are to be used to remove keys.
+ * @return Returns true if any keys were removed from the array map, else false.
+ */
+ open fun removeAll(collection: Collection<K>): Boolean {
+ val oldSize = _size
+ for (o in collection) {
+ remove(o)
+ }
+ return oldSize != _size
+ }
+
+ /**
+ * Remove all keys in the array map that do <b>not</b> exist in the given collection.
+ * @param collection The collection whose contents are to be used to determine which
+ * keys to keep.
+ * @return Returns true if any keys were removed from the array map, else false.
+ */
+ open fun retainAll(collection: Collection<K>): Boolean {
+ val oldSize = _size
+ for (i in _size downTo 0) {
+ if (!collection.contains(keyAt(i))) {
+ removeAt(i)
+ }
+ }
+ return oldSize != _size
+ }
+
+ /**
+ * Return a {@link Set} for iterating over and interacting with all mappings
+ * in the array map.
+ *
+ * <p><b>Note:</b> this is a very inefficient way to access the array contents, it
+ * requires generating a number of temporary objects.</p>
+ *
+ * <p><b>Note:</b></p> the semantics of this
+ * Set are subtly different than that of a {@link HashMap}: most important,
+ * the {@link Map.Entry Map.Entry} object returned by its iterator is a single
+ * object that exists for the entire iterator, so you can <b>not</b> hold on to it
+ * after calling {@link Iterator#next() Iterator.next}.</p>
+ */
+ override val entries: MutableSet<MutableMap.MutableEntry<K, V>>
+ get() {
+ return _entries ?: EntrySet(this).also { _entries = it }
+ }
+
+ private var _entries: EntrySet<K, V>? = null
+
+ /**
+ * Return a {@link Set} for iterating over and interacting with all keys
+ * in the array map.
+ *
+ * <p><b>Note:</b> this is a fairly inefficient way to access the array contents, it
+ * requires generating a number of temporary objects.</p>
+ */
+ override val keys: MutableSet<K>
+ get() {
+ return _keys ?: KeySet(this).also { _keys = it }
+ }
+
+ private var _keys: KeySet<K, V>? = null
+
+ /**
+ * Return a {@link Collection} for iterating over and interacting with all values
+ * in the array map.
+ *
+ * <p><b>Note:</b> this is a fairly inefficient way to access the array contents, it
+ * requires generating a number of temporary objects.</p>
+ */
+ override val values: MutableCollection<V>
+ get() = _values ?: ValueCollection(this).also { _values = it }
+
+ private var _values: ValueCollection<K, V>? = null
+
+ // Required by compiler.
+ open override fun getOrDefault(key: K, defaultValue: V): V =
+ super<SimpleArrayMap>.getOrDefault(
+ key, defaultValue
+ )
+
+ // Required by compiler.
+ open override fun remove(key: K, value: V): Boolean = super<SimpleArrayMap>.remove(
+ key,
+ value
+ )
+
+ // Required by compiler.
+ open override fun putIfAbsent(key: K, value: V): V? =
+ super<SimpleArrayMap>.putIfAbsent(key, value)
+
+ // Required by compiler.
+ open override fun replace(key: K, value: V): V? =
+ super<SimpleArrayMap>.replace(key, value)
+
+ // Required by compiler.
+ open override fun replace(key: K, oldValue: V, newValue: V): Boolean =
+ super<SimpleArrayMap>.replace(key, oldValue, newValue)
+}
diff --git a/collection/collection2/src/nonJvmMain/kotlin/androidx/collection/LongSparseArray.kt b/collection/collection2/src/nonJvmMain/kotlin/androidx/collection/LongSparseArray.kt
new file mode 100644
index 0000000..f2f7005
--- /dev/null
+++ b/collection/collection2/src/nonJvmMain/kotlin/androidx/collection/LongSparseArray.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+actual class LongSparseArray<E> actual constructor(initialCapacity: Int) {
+ internal actual var keys: LongArray
+ internal actual var values: Array<Any?>
+ init {
+ if (initialCapacity == 0) {
+ keys = EMPTY_LONGS
+ values = EMPTY_OBJECTS
+ } else {
+ val length = idealLongArraySize(initialCapacity)
+ keys = LongArray(length)
+ values = arrayOfNulls(length)
+ }
+ }
+
+ internal actual var garbage: Boolean = false
+ @Suppress("PropertyName") // Normal backing field name but internal for common code.
+ internal actual var _size: Int = 0
+
+ actual constructor(array: LongSparseArray<E>) : this(0) {
+ _size = array._size
+ keys = array.keys.copyOf()
+ values = array.values.copyOf()
+ garbage = array.garbage
+ gc()
+ }
+
+ actual val size: Int get() = commonSize()
+
+ actual fun isEmpty(): Boolean = commonIsEmpty()
+
+ actual operator fun get(key: Long): E? = commonGet(key, null)
+ actual fun get(key: Long, default: E): E = commonGet(key, default)
+
+ actual fun put(key: Long, value: E): Unit = commonPut(key, value)
+ actual fun putAll(other: LongSparseArray<out E>): Unit = commonPutAll(other)
+ actual fun putIfAbsent(key: Long, value: E): E? = commonPutIfAbsent(key, value)
+ actual fun append(key: Long, value: E): Unit = commonAppend(key, value)
+
+ actual fun keyAt(index: Int): Long = commonKeyAt(index)
+
+ actual fun valueAt(index: Int): E = commonValueAt(index)
+ actual fun setValueAt(index: Int, value: E): Unit = commonSetValueAt(index, value)
+
+ actual fun indexOfKey(key: Long): Int = commonIndexOfKey(key)
+ actual fun indexOfValue(value: E): Int = commonIndexOfValue(value)
+
+ actual fun containsKey(key: Long): Boolean = commonContainsKey(key)
+ actual fun containsValue(value: E): Boolean = commonContainsValue(value)
+
+ actual fun clear(): Unit = commonClear()
+
+ actual fun remove(key: Long): Unit = commonRemove(key)
+ actual fun remove(key: Long, value: Any?): Boolean = commonRemove(key, value)
+ actual fun removeAt(index: Int): Unit = commonRemoveAt(index)
+
+ actual fun replace(key: Long, value: E): E? = commonReplace(key, value)
+ actual fun replace(key: Long, oldValue: E?, newValue: E): Boolean =
+ commonReplace(key, oldValue, newValue)
+
+ override fun toString(): String = commonToString()
+}
diff --git a/collection/collection2/src/nonJvmMain/kotlin/androidx/collection/Platform.kt b/collection/collection2/src/nonJvmMain/kotlin/androidx/collection/Platform.kt
new file mode 100644
index 0000000..d3dd3ab
--- /dev/null
+++ b/collection/collection2/src/nonJvmMain/kotlin/androidx/collection/Platform.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.collection
+
+import kotlinx.atomicfu.locks.synchronized
+
+internal actual fun indexOutOfBounds(): IndexOutOfBoundsException {
+ return IndexOutOfBoundsException()
+}
+
+internal actual class SynchronizedObject : kotlinx.atomicfu.locks.SynchronizedObject()
+
+internal actual fun createSynchronizedObject() = SynchronizedObject()
+
+/**
+ * TODO(b/172658775): Stop using AtomicFU on Apple platforms.
+ *
+ * Benchmarks showed that it led to significant slowdowns. Native synchronization intrinsics would
+ * be a better solution.
+ */
+internal actual inline fun <R> synchronizedOperation(lock: SynchronizedObject, block: () -> R): R {
+ return synchronized(lock, block)
+}
diff --git a/collection/collection2/src/nonJvmMain/kotlin/androidx/collection/SparseArray.kt b/collection/collection2/src/nonJvmMain/kotlin/androidx/collection/SparseArray.kt
new file mode 100644
index 0000000..3a89403
--- /dev/null
+++ b/collection/collection2/src/nonJvmMain/kotlin/androidx/collection/SparseArray.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+actual class SparseArray<E> actual constructor(initialCapacity: Int) {
+ internal actual var keys: IntArray
+ internal actual var values: Array<Any?>
+ init {
+ if (initialCapacity == 0) {
+ keys = EMPTY_INTS
+ values = EMPTY_OBJECTS
+ } else {
+ val length = idealIntArraySize(initialCapacity)
+ keys = IntArray(length)
+ values = arrayOfNulls(length)
+ }
+ }
+
+ internal actual var garbage: Boolean = false
+ @Suppress("PropertyName") // Normal backing field name but internal for common code.
+ internal actual var _size: Int = 0
+
+ actual constructor(array: SparseArray<E>) : this(0) {
+ _size = array._size
+ keys = array.keys.copyOf()
+ values = array.values.copyOf()
+ garbage = array.garbage
+ gc()
+ }
+
+ actual val size: Int get() = commonSize()
+
+ actual fun isEmpty(): Boolean = commonIsEmpty()
+
+ actual operator fun get(key: Int): E? = commonGet(key, null)
+ actual fun get(key: Int, default: E): E = commonGet(key, default)
+
+ actual fun put(key: Int, value: E): Unit = commonPut(key, value)
+ actual fun putAll(other: SparseArray<out E>): Unit = commonPutAll(other)
+ actual fun putIfAbsent(key: Int, value: E): E? = commonPutIfAbsent(key, value)
+ actual fun append(key: Int, value: E): Unit = commonAppend(key, value)
+
+ actual fun keyAt(index: Int): Int = commonKeyAt(index)
+
+ actual fun valueAt(index: Int): E = commonValueAt(index)
+ actual fun setValueAt(index: Int, value: E): Unit = commonSetValueAt(index, value)
+
+ actual fun indexOfKey(key: Int): Int = commonIndexOfKey(key)
+ actual fun indexOfValue(value: E): Int = commonIndexOfValue(value)
+
+ actual fun containsKey(key: Int): Boolean = commonContainsKey(key)
+ actual fun containsValue(value: E): Boolean = commonContainsValue(value)
+
+ actual fun clear(): Unit = commonClear()
+
+ actual fun remove(key: Int): Unit = commonRemove(key)
+ actual fun remove(key: Int, value: Any?): Boolean = commonRemove(key, value)
+ actual fun removeAt(index: Int): Unit = commonRemoveAt(index)
+
+ actual fun replace(key: Int, value: E): E? = commonReplace(key, value)
+ actual fun replace(key: Int, oldValue: E?, newValue: E): Boolean =
+ commonReplace(key, oldValue, newValue)
+
+ override fun toString(): String = commonToString()
+}
diff --git a/compose/animation/animation-core-lint/src/test/java/androidx/compose/animation/core/lint/TransitionDetectorTest.kt b/compose/animation/animation-core-lint/src/test/java/androidx/compose/animation/core/lint/TransitionDetectorTest.kt
index 38d459a..47fffe8 100644
--- a/compose/animation/animation-core-lint/src/test/java/androidx/compose/animation/core/lint/TransitionDetectorTest.kt
+++ b/compose/animation/animation-core-lint/src/test/java/androidx/compose/animation/core/lint/TransitionDetectorTest.kt
@@ -23,6 +23,7 @@
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -148,6 +149,7 @@
"""
)
+ @Ignore // Started failing with AGP 7.1.0-alpha03 upgrade
@Test
fun unreferencedParameters() {
lint().files(
@@ -202,6 +204,7 @@
)
}
+ @Ignore // Started failing with AGP 7.1.0-alpha03 upgrade
@Test
fun unreferencedParameter_shadowedNames() {
lint().files(
diff --git a/compose/animation/animation-graphics/OWNERS b/compose/animation/animation-graphics/OWNERS
new file mode 100644
index 0000000..76a468f
--- /dev/null
+++ b/compose/animation/animation-graphics/OWNERS
@@ -0,0 +1,3 @@
+njawad@google.com
+tianliu@google.com
+yaraki@google.com
diff --git a/compose/animation/animation-graphics/api/current.txt b/compose/animation/animation-graphics/api/current.txt
new file mode 100644
index 0000000..5522e93
--- /dev/null
+++ b/compose/animation/animation-graphics/api/current.txt
@@ -0,0 +1,31 @@
+// Signature format: 4.0
+package androidx.compose.animation.graphics.res {
+
+ public final class AnimatedVectorResources_androidKt {
+ }
+
+ public final class AnimatorResources_androidKt {
+ }
+
+}
+
+package androidx.compose.animation.graphics.vector {
+
+ public final class AnimatorKt {
+ }
+
+}
+
+package androidx.compose.animation.graphics.vector.compat {
+
+ public final class XmlAnimatedVectorParser_androidKt {
+ }
+
+ public final class XmlAnimatorParser_androidKt {
+ }
+
+ public final class XmlPullParserUtils_androidKt {
+ }
+
+}
+
diff --git a/compose/animation/animation-graphics/api/public_plus_experimental_current.txt b/compose/animation/animation-graphics/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..b3d6b90
--- /dev/null
+++ b/compose/animation/animation-graphics/api/public_plus_experimental_current.txt
@@ -0,0 +1,47 @@
+// Signature format: 4.0
+package androidx.compose.animation.graphics {
+
+ @kotlin.RequiresOptIn(message="This is an experimental animation graphics API.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ExperimentalAnimationGraphicsApi {
+ }
+
+}
+
+package androidx.compose.animation.graphics.res {
+
+ public final class AnimatedVectorResources_androidKt {
+ method @androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi @androidx.compose.runtime.Composable public static androidx.compose.animation.graphics.vector.AnimatedImageVector animatedVectorResource(@DrawableRes int id);
+ }
+
+ public final class AnimatorResources_androidKt {
+ }
+
+}
+
+package androidx.compose.animation.graphics.vector {
+
+ @androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi @androidx.compose.runtime.Immutable public final class AnimatedImageVector {
+ method public androidx.compose.ui.graphics.vector.ImageVector getImageVector();
+ method public int getTotalDuration();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.painter.Painter painterFor(boolean atEnd);
+ property public final androidx.compose.ui.graphics.vector.ImageVector imageVector;
+ property public final int totalDuration;
+ }
+
+ public final class AnimatorKt {
+ }
+
+}
+
+package androidx.compose.animation.graphics.vector.compat {
+
+ public final class XmlAnimatedVectorParser_androidKt {
+ }
+
+ public final class XmlAnimatorParser_androidKt {
+ }
+
+ public final class XmlPullParserUtils_androidKt {
+ }
+
+}
+
diff --git a/compose/animation/animation-graphics/api/res-current.txt b/compose/animation/animation-graphics/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/compose/animation/animation-graphics/api/res-current.txt
diff --git a/compose/animation/animation-graphics/api/restricted_current.txt b/compose/animation/animation-graphics/api/restricted_current.txt
new file mode 100644
index 0000000..5522e93
--- /dev/null
+++ b/compose/animation/animation-graphics/api/restricted_current.txt
@@ -0,0 +1,31 @@
+// Signature format: 4.0
+package androidx.compose.animation.graphics.res {
+
+ public final class AnimatedVectorResources_androidKt {
+ }
+
+ public final class AnimatorResources_androidKt {
+ }
+
+}
+
+package androidx.compose.animation.graphics.vector {
+
+ public final class AnimatorKt {
+ }
+
+}
+
+package androidx.compose.animation.graphics.vector.compat {
+
+ public final class XmlAnimatedVectorParser_androidKt {
+ }
+
+ public final class XmlAnimatorParser_androidKt {
+ }
+
+ public final class XmlPullParserUtils_androidKt {
+ }
+
+}
+
diff --git a/compose/animation/animation-graphics/build.gradle b/compose/animation/animation-graphics/build.gradle
new file mode 100644
index 0000000..c222c21
--- /dev/null
+++ b/compose/animation/animation-graphics/build.gradle
@@ -0,0 +1,119 @@
+/*
+ * 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.
+ */
+
+
+import androidx.build.AndroidXComposePlugin
+import androidx.build.LibraryGroups
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXComposePlugin")
+}
+
+AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+
+dependencies {
+ kotlinPlugin(project(":compose:compiler:compiler"))
+
+ if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+ /*
+ * When updating dependencies, make sure to make the an an analogous update in the
+ * corresponding block below
+ */
+
+ api("androidx.annotation:annotation:1.1.0")
+ api(project(":compose:animation:animation"))
+ api(project(":compose:foundation:foundation-layout"))
+ api(project(":compose:runtime:runtime"))
+ api(project(":compose:ui:ui"))
+ api(project(":compose:ui:ui-geometry"))
+
+ implementation(project(":compose:ui:ui-util"))
+ implementation(libs.kotlinStdlibCommon)
+ implementation("androidx.core:core-ktx:1.5.0")
+
+ testImplementation(libs.testRules)
+ testImplementation(libs.testRunner)
+ testImplementation(libs.junit)
+
+ androidTestImplementation(project(":compose:foundation:foundation"))
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(project(":compose:test-utils"))
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.truth)
+ }
+}
+
+if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+ kotlin {
+ android()
+ jvm("desktop")
+
+ /*
+ * When updating dependencies, make sure to make the an an analogous update in the
+ * corresponding block above
+ */
+ sourceSets {
+ commonMain.dependencies {
+ implementation(libs.kotlinStdlibCommon)
+
+ api(project(":compose:animation:animation"))
+ api(project(":compose:foundation:foundation-layout"))
+ api(project(":compose:runtime:runtime"))
+ api(project(":compose:ui:ui"))
+ api(project(":compose:ui:ui-geometry"))
+
+ implementation(project(":compose:ui:ui-util"))
+ }
+
+ androidMain.dependencies {
+ api("androidx.annotation:annotation:1.1.0")
+ implementation("androidx.core:core-ktx:1.5.0")
+ }
+
+ desktopMain.dependencies {
+ implementation(libs.kotlinStdlib)
+ }
+
+ androidTest.dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.junit)
+ }
+
+ androidAndroidTest.dependencies {
+ implementation(libs.testRules)
+ implementation(libs.testRunner)
+ implementation(libs.junit)
+ implementation(project(":compose:foundation:foundation"))
+ implementation(project(":compose:ui:ui-test-junit4"))
+ implementation(project(":compose:test-utils"))
+ }
+ }
+ }
+}
+
+androidx {
+ name = "Compose Animation Graphics"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenGroup = LibraryGroups.Compose.ANIMATION
+ inceptionYear = "2021"
+ description = "Compose Animation Graphics Library for using animated-vector resources in Compose"
+}
diff --git a/compose/animation/animation-graphics/samples/build.gradle b/compose/animation/animation-graphics/samples/build.gradle
new file mode 100644
index 0000000..6f2593e
--- /dev/null
+++ b/compose/animation/animation-graphics/samples/build.gradle
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXComposePlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ kotlinPlugin(project(":compose:compiler:compiler"))
+
+ implementation(libs.kotlinStdlib)
+
+ compileOnly(project(":annotation:annotation-sampled"))
+
+ implementation(project(":compose:animation:animation"))
+ implementation(project(":compose:animation:animation-graphics"))
+ implementation(project(":compose:foundation:foundation"))
+ implementation(project(":compose:material:material"))
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:ui:ui-text"))
+}
+
+androidx {
+ name = "AndroidX Compose UI Animation Graphics Library Samples"
+ type = LibraryType.SAMPLES
+ mavenGroup = LibraryGroups.Compose.ANIMATION
+ inceptionYear = "2021"
+ description = "Contains the sample code for the Androidx Compose UI Animation Graphics Library"
+}
diff --git a/benchmark/integration-tests/crystalball-experiment/src/main/AndroidManifest.xml b/compose/animation/animation-graphics/samples/src/main/AndroidManifest.xml
similarity index 83%
copy from benchmark/integration-tests/crystalball-experiment/src/main/AndroidManifest.xml
copy to compose/animation/animation-graphics/samples/src/main/AndroidManifest.xml
index a599d46..3ea924a 100644
--- a/benchmark/integration-tests/crystalball-experiment/src/main/AndroidManifest.xml
+++ b/compose/animation/animation-graphics/samples/src/main/AndroidManifest.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 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.
@@ -13,5 +13,5 @@
~ 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"/>
+-->
+<manifest package="androidx.compose.animation.graphics.samples" />
diff --git a/compose/animation/animation-graphics/samples/src/main/java/androidx/compose/animation/graphics/samples/AnimatedVectorSample.kt b/compose/animation/animation-graphics/samples/src/main/java/androidx/compose/animation/graphics/samples/AnimatedVectorSample.kt
new file mode 100644
index 0000000..e660d5b
--- /dev/null
+++ b/compose/animation/animation-graphics/samples/src/main/java/androidx/compose/animation/graphics/samples/AnimatedVectorSample.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.animation.graphics.samples
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.Sampled
+import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
+import androidx.compose.animation.graphics.res.animatedVectorResource
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Sampled
+@Composable
+fun AnimatedVectorSample() {
+
+ @OptIn(ExperimentalAnimationGraphicsApi::class)
+ @Composable
+ fun AnimatedVector(@DrawableRes drawableId: Int) {
+ val image = animatedVectorResource(drawableId)
+ var atEnd by remember { mutableStateOf(false) }
+ Image(
+ painter = image.painterFor(atEnd),
+ contentDescription = "Your content description",
+ modifier = Modifier.size(64.dp).clickable {
+ atEnd = !atEnd
+ }
+ )
+ }
+}
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/AndroidManifest.xml b/compose/animation/animation-graphics/src/androidAndroidTest/AndroidManifest.xml
new file mode 100644
index 0000000..efb54b0
--- /dev/null
+++ b/compose/animation/animation-graphics/src/androidAndroidTest/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?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.compose.animation.graphics" />
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/kotlin/androidx/compose/animation/graphics/vector/compat/XmlAnimatedVectorParserTest.kt b/compose/animation/animation-graphics/src/androidAndroidTest/kotlin/androidx/compose/animation/graphics/vector/compat/XmlAnimatedVectorParserTest.kt
new file mode 100644
index 0000000..bc16e765
--- /dev/null
+++ b/compose/animation/animation-graphics/src/androidAndroidTest/kotlin/androidx/compose/animation/graphics/vector/compat/XmlAnimatedVectorParserTest.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.animation.graphics.vector.compat
+
+import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
+import androidx.compose.animation.graphics.res.loadAnimatedVectorResource
+import androidx.compose.animation.graphics.test.R
+import androidx.compose.animation.graphics.vector.AnimatorSet
+import androidx.compose.animation.graphics.vector.ObjectAnimator
+import androidx.compose.animation.graphics.vector.PropertyValuesHolderColor
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalAnimationGraphicsApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class XmlAnimatedVectorParserTest {
+
+ @Test
+ fun load() {
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ val resources = context.resources
+ val avd = loadAnimatedVectorResource(
+ context.theme,
+ resources,
+ R.drawable.avd_complex
+ )
+
+ val delta = 0.001f
+ assertThat(avd.imageVector.defaultWidth).isEqualTo(24.dp)
+ assertThat(avd.imageVector.defaultHeight).isEqualTo(24.dp)
+ assertThat(avd.imageVector.viewportWidth).isWithin(delta).of(24f)
+ assertThat(avd.imageVector.viewportHeight).isWithin(delta).of(24f)
+
+ assertThat(avd.targets).hasSize(1)
+
+ avd.targets[0].let { target ->
+ assertThat(target.name).isEqualTo("background")
+ assertThat(target.animator).isInstanceOf(AnimatorSet::class.java)
+ (target.animator as AnimatorSet).let { set ->
+ assertThat(set.animators).hasSize(1)
+ (set.animators[0] as ObjectAnimator).let { a ->
+ assertThat(a.duration).isEqualTo(123)
+ assertThat(a.holders).hasSize(1)
+ (a.holders[0] as PropertyValuesHolderColor).let { holder ->
+ assertThat(holder.propertyName).isEqualTo("fillColor")
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/kotlin/androidx/compose/animation/graphics/vector/compat/XmlAnimatorParserTest.kt b/compose/animation/animation-graphics/src/androidAndroidTest/kotlin/androidx/compose/animation/graphics/vector/compat/XmlAnimatorParserTest.kt
new file mode 100644
index 0000000..abf0e47
--- /dev/null
+++ b/compose/animation/animation-graphics/src/androidAndroidTest/kotlin/androidx/compose/animation/graphics/vector/compat/XmlAnimatorParserTest.kt
@@ -0,0 +1,197 @@
+/*
+ * 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.animation.graphics.vector.compat
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.ui.graphics.Color
+import androidx.compose.animation.graphics.vector.AnimatorSet
+import androidx.compose.animation.graphics.vector.ObjectAnimator
+import androidx.compose.animation.graphics.vector.Ordering
+import androidx.compose.animation.graphics.vector.PropertyValuesHolder2D
+import androidx.compose.animation.graphics.vector.PropertyValuesHolderColor
+import androidx.compose.animation.graphics.vector.PropertyValuesHolderFloat
+import androidx.compose.animation.graphics.vector.PropertyValuesHolderInt
+import androidx.compose.animation.graphics.vector.PropertyValuesHolderPath
+import androidx.compose.animation.graphics.res.AccelerateEasing
+import androidx.compose.animation.graphics.res.DecelerateEasing
+import androidx.compose.animation.graphics.res.loadAnimatorResource
+import androidx.compose.animation.graphics.test.R
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class XmlAnimatorParserTest {
+
+ @Test
+ fun objectAnimator1D() {
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ val resources = context.resources
+ val delta = 0.001f
+ val a = loadAnimatorResource(
+ context.theme,
+ resources,
+ R.animator.object_animator_1d
+ )
+ assertThat(a).isInstanceOf(ObjectAnimator::class.java)
+ val oa = a as ObjectAnimator
+ assertThat(oa.duration).isEqualTo(333)
+ assertThat(oa.repeatCount).isEqualTo(1)
+ assertThat(oa.repeatMode).isEqualTo(RepeatMode.Reverse)
+ assertThat(oa.startDelay).isEqualTo(50)
+ assertThat(oa.holders).hasSize(1)
+ assertThat(oa.totalDuration).isEqualTo(716)
+ val holder = oa.holders[0] as PropertyValuesHolderFloat
+ assertThat(holder.propertyName).isEqualTo("translateX")
+ assertThat(holder.animatorKeyframes).hasSize(2)
+ assertThat(holder.animatorKeyframes[0].value).isWithin(delta).of(0f)
+ assertThat(holder.animatorKeyframes[1].value).isWithin(delta).of(100f)
+ }
+
+ @Test
+ fun objectAnimator2D() {
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ val resources = context.resources
+ val a = loadAnimatorResource(
+ context.theme,
+ resources,
+ R.animator.object_animator_2d
+ )
+ assertThat(a).isInstanceOf(ObjectAnimator::class.java)
+ val oa = a as ObjectAnimator
+ assertThat(oa.duration).isEqualTo(333)
+ assertThat(oa.holders).hasSize(1)
+ assertThat(oa.totalDuration).isEqualTo(333)
+ val holder = oa.holders[0] as PropertyValuesHolder2D
+ assertThat(holder.xPropertyName).isEqualTo("translateX")
+ assertThat(holder.yPropertyName).isEqualTo("translateY")
+ assertThat(holder.pathData).hasSize(3)
+ assertThat(holder.interpolator).isEqualTo(LinearEasing)
+ }
+
+ @Test
+ fun propertyValuesHolders() {
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ val resources = context.resources
+ val delta = 0.001f
+ val a = loadAnimatorResource(
+ context.theme,
+ resources,
+ R.animator.property_values_holders
+ )
+ assertThat(a).isInstanceOf(ObjectAnimator::class.java)
+ val oa = a as ObjectAnimator
+ assertThat(oa.duration).isEqualTo(333)
+ assertThat(oa.repeatCount).isEqualTo(1)
+ assertThat(oa.repeatMode).isEqualTo(RepeatMode.Reverse)
+ assertThat(oa.startDelay).isEqualTo(50)
+ assertThat(oa.holders).hasSize(5)
+ assertThat(oa.totalDuration).isEqualTo(716)
+ (oa.holders[0] as PropertyValuesHolderFloat).let { holder ->
+ assertThat(holder.propertyName).isEqualTo("translateX")
+ assertThat(holder.animatorKeyframes).hasSize(2)
+ assertThat(holder.animatorKeyframes[0].value).isWithin(delta).of(0f)
+ assertThat(holder.animatorKeyframes[1].value).isWithin(delta).of(100f)
+ }
+ (oa.holders[1] as PropertyValuesHolderFloat).let { holder ->
+ assertThat(holder.propertyName).isEqualTo("translateY")
+ assertThat(holder.animatorKeyframes).hasSize(4)
+ holder.animatorKeyframes[0].let { keyframe ->
+ assertThat(keyframe.fraction).isWithin(delta).of(0f)
+ assertThat(keyframe.value).isWithin(delta).of(0f)
+ }
+ holder.animatorKeyframes[1].let { keyframe ->
+ assertThat(keyframe.fraction).isWithin(delta).of(0.3f)
+ assertThat(keyframe.value).isWithin(delta).of(150f)
+ assertThat(keyframe.interpolator).isEqualTo(DecelerateEasing)
+ }
+ holder.animatorKeyframes[2].let { keyframe ->
+ assertThat(keyframe.fraction).isWithin(delta).of(0.6f)
+ assertThat(keyframe.value).isWithin(delta).of(50f)
+ assertThat(keyframe.interpolator).isEqualTo(AccelerateEasing)
+ }
+ holder.animatorKeyframes[3].let { keyframe ->
+ assertThat(keyframe.fraction).isWithin(delta).of(1f)
+ assertThat(keyframe.value).isWithin(delta).of(200f)
+ }
+ }
+ (oa.holders[2] as PropertyValuesHolderColor).let { holder ->
+ assertThat(holder.propertyName).isEqualTo("colorProperty")
+ assertThat(holder.animatorKeyframes).hasSize(2)
+ assertThat(holder.animatorKeyframes[0].value).isEqualTo(Color.Red)
+ assertThat(holder.animatorKeyframes[1].value).isEqualTo(Color.Blue)
+ }
+ (oa.holders[3] as PropertyValuesHolderInt).let { holder ->
+ assertThat(holder.propertyName).isEqualTo("intProperty")
+ assertThat(holder.animatorKeyframes).hasSize(2)
+ assertThat(holder.animatorKeyframes[0].value).isEqualTo(500)
+ assertThat(holder.animatorKeyframes[1].value).isEqualTo(1000)
+ }
+ (oa.holders[4] as PropertyValuesHolderPath).let { holder ->
+ assertThat(holder.propertyName).isEqualTo("pathProperty")
+ assertThat(holder.animatorKeyframes).hasSize(2)
+ assertThat(holder.animatorKeyframes[0].value).hasSize(3)
+ assertThat(holder.animatorKeyframes[1].value).hasSize(3)
+ }
+ }
+
+ @Test
+ fun set() {
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ val resources = context.resources
+ val anim = loadAnimatorResource(
+ context.theme,
+ resources,
+ R.animator.set
+ )
+ assertThat(anim).isInstanceOf(AnimatorSet::class.java)
+ val set = anim as AnimatorSet
+ assertThat(set.ordering).isEqualTo(Ordering.Together)
+ assertThat(set.animators).hasSize(2)
+ assertThat(set.totalDuration).isEqualTo(300)
+ (set.animators[0] as ObjectAnimator).let { oa ->
+ assertThat(oa.duration).isEqualTo(300)
+ assertThat(oa.repeatCount).isEqualTo(0)
+ assertThat(oa.startDelay).isEqualTo(0)
+ assertThat(oa.holders).hasSize(1)
+ (oa.holders[0] as PropertyValuesHolderFloat).let { holder ->
+ assertThat(holder.propertyName).isEqualTo("floatProperty")
+ }
+ }
+ (set.animators[1] as AnimatorSet).let { s ->
+ assertThat(s.ordering).isEqualTo(Ordering.Sequentially)
+ assertThat(s.animators).hasSize(2)
+ (s.animators[0] as ObjectAnimator).let { oa ->
+ assertThat(oa.holders).hasSize(1)
+ (oa.holders[0] as PropertyValuesHolderInt).let { holder ->
+ assertThat(holder.propertyName).isEqualTo("intProperty")
+ }
+ }
+ (s.animators[1] as ObjectAnimator).let { oa ->
+ assertThat(oa.holders).hasSize(1)
+ (oa.holders[0] as PropertyValuesHolderColor).let { holder ->
+ assertThat(holder.propertyName).isEqualTo("colorProperty")
+ }
+ }
+ }
+ }
+}
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/res/animator/complex_background.xml b/compose/animation/animation-graphics/src/androidAndroidTest/res/animator/complex_background.xml
new file mode 100644
index 0000000..3de4e5b
--- /dev/null
+++ b/compose/animation/animation-graphics/src/androidAndroidTest/res/animator/complex_background.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <objectAnimator
+ android:duration="123"
+ android:propertyName="fillColor"
+ android:valueFrom="#777777"
+ android:valueTo="#999999"
+ android:valueType="colorType" />
+</set>
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/res/animator/object_animator_1d.xml b/compose/animation/animation-graphics/src/androidAndroidTest/res/animator/object_animator_1d.xml
new file mode 100644
index 0000000..ab5497f
--- /dev/null
+++ b/compose/animation/animation-graphics/src/androidAndroidTest/res/animator/object_animator_1d.xml
@@ -0,0 +1,26 @@
+<?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.
+-->
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="333"
+ android:interpolator="@android:anim/linear_interpolator"
+ android:propertyName="translateX"
+ android:repeatCount="1"
+ android:repeatMode="reverse"
+ android:startOffset="50"
+ android:valueFrom="0"
+ android:valueTo="100"
+ android:valueType="floatType" />
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/res/animator/object_animator_2d.xml b/compose/animation/animation-graphics/src/androidAndroidTest/res/animator/object_animator_2d.xml
new file mode 100644
index 0000000..01af795
--- /dev/null
+++ b/compose/animation/animation-graphics/src/androidAndroidTest/res/animator/object_animator_2d.xml
@@ -0,0 +1,22 @@
+<?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.
+-->
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="333"
+ android:interpolator="@android:anim/linear_interpolator"
+ android:pathData="L30,40 L70,60 L100,100"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY" />
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/res/animator/property_values_holders.xml b/compose/animation/animation-graphics/src/androidAndroidTest/res/animator/property_values_holders.xml
new file mode 100644
index 0000000..ffc7eeb
--- /dev/null
+++ b/compose/animation/animation-graphics/src/androidAndroidTest/res/animator/property_values_holders.xml
@@ -0,0 +1,56 @@
+<?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.
+-->
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="333"
+ android:interpolator="@android:anim/linear_interpolator"
+ android:repeatCount="1"
+ android:repeatMode="reverse"
+ android:startOffset="50">
+ <propertyValuesHolder
+ android:propertyName="translateX"
+ android:valueFrom="0"
+ android:valueTo="100"
+ android:valueType="floatType" />
+ <propertyValuesHolder
+ android:propertyName="translateY"
+ android:valueFrom="0"
+ android:valueTo="200">
+ <keyframe
+ android:fraction="0.3"
+ android:interpolator="@android:anim/decelerate_interpolator"
+ android:value="150" />
+ <keyframe
+ android:fraction="0.6"
+ android:interpolator="@android:anim/accelerate_interpolator"
+ android:value="50" />
+ </propertyValuesHolder>
+ <propertyValuesHolder
+ android:propertyName="colorProperty"
+ android:valueFrom="#ff0000"
+ android:valueTo="#0000ff"
+ android:valueType="colorType" />
+ <propertyValuesHolder
+ android:propertyName="intProperty"
+ android:valueFrom="500"
+ android:valueTo="1000"
+ android:valueType="intType" />
+ <propertyValuesHolder
+ android:propertyName="pathProperty"
+ android:valueFrom="L24,0 L24,24 Z"
+ android:valueTo="L24,24 L0,24 Z"
+ android:valueType="pathType" />
+</objectAnimator>
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/res/animator/set.xml b/compose/animation/animation-graphics/src/androidAndroidTest/res/animator/set.xml
new file mode 100644
index 0000000..d674d02
--- /dev/null
+++ b/compose/animation/animation-graphics/src/androidAndroidTest/res/animator/set.xml
@@ -0,0 +1,38 @@
+<?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.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <objectAnimator
+ android:duration="300"
+ android:propertyName="floatProperty"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:valueType="floatType" />
+ <set android:ordering="sequentially">
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="intProperty"
+ android:valueFrom="100"
+ android:valueTo="200"
+ android:valueType="intType" />
+ <objectAnimator
+ android:duration="200"
+ android:propertyName="colorProperty"
+ android:valueFrom="#ff0000"
+ android:valueTo="#0000ff"
+ android:valueType="colorType" />
+ </set>
+</set>
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/res/drawable/avd_complex.xml b/compose/animation/animation-graphics/src/androidAndroidTest/res/drawable/avd_complex.xml
new file mode 100644
index 0000000..18ff685
--- /dev/null
+++ b/compose/animation/animation-graphics/src/androidAndroidTest/res/drawable/avd_complex.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/vd_complex">
+
+ <target
+ android:name="background"
+ android:animation="@animator/complex_background" />
+
+</animated-vector>
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/res/drawable/vd_complex.xml b/compose/animation/animation-graphics/src/androidAndroidTest/res/drawable/vd_complex.xml
new file mode 100644
index 0000000..3a96ae2
--- /dev/null
+++ b/compose/animation/animation-graphics/src/androidAndroidTest/res/drawable/vd_complex.xml
@@ -0,0 +1,26 @@
+<?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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <path
+ android:name="background"
+ android:fillColor="#777777"
+ android:pathData="L24,0 24,24 0,24Z" />
+</vector>
diff --git a/benchmark/integration-tests/crystalball-experiment/src/main/AndroidManifest.xml b/compose/animation/animation-graphics/src/androidMain/AndroidManifest.xml
similarity index 84%
rename from benchmark/integration-tests/crystalball-experiment/src/main/AndroidManifest.xml
rename to compose/animation/animation-graphics/src/androidMain/AndroidManifest.xml
index a599d46..91a8747 100644
--- a/benchmark/integration-tests/crystalball-experiment/src/main/AndroidManifest.xml
+++ b/compose/animation/animation-graphics/src/androidMain/AndroidManifest.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 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.
@@ -13,5 +13,5 @@
~ 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"/>
+-->
+<manifest package="androidx.compose.animation.graphics" />
diff --git a/compose/animation/animation-graphics/src/androidMain/kotlin/androidx/compose/animation/graphics/res/AnimatedVectorResources.android.kt b/compose/animation/animation-graphics/src/androidMain/kotlin/androidx/compose/animation/graphics/res/AnimatedVectorResources.android.kt
new file mode 100644
index 0000000..b7f979c
--- /dev/null
+++ b/compose/animation/animation-graphics/src/androidMain/kotlin/androidx/compose/animation/graphics/res/AnimatedVectorResources.android.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.animation.graphics.res
+
+import android.content.res.Resources
+import android.util.Xml
+import androidx.annotation.DrawableRes
+import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.animation.graphics.vector.AnimatedImageVector
+import androidx.compose.animation.graphics.vector.compat.parseAnimatedImageVector
+import androidx.compose.animation.graphics.vector.compat.seekToStartTag
+import androidx.compose.ui.platform.LocalContext
+import org.xmlpull.v1.XmlPullParserException
+
+/**
+ * Load an [AnimatedImageVector] from an Android resource id.
+ *
+ * Note: This API is transient and will be likely removed for encouraging async resource loading.
+ *
+ * @param id the resource identifier
+ * @return an animated vector drawable resource.
+ *
+ * @sample androidx.compose.animation.graphics.samples.AnimatedVectorSample
+ */
+@ExperimentalAnimationGraphicsApi
+@Composable
+fun animatedVectorResource(@DrawableRes id: Int): AnimatedImageVector {
+ val context = LocalContext.current
+ val res = context.resources
+ val theme = context.theme
+ return remember(id) {
+ loadAnimatedVectorResource(theme, res, id)
+ }
+}
+
+@ExperimentalAnimationGraphicsApi
+@Throws(XmlPullParserException::class)
+internal fun loadAnimatedVectorResource(
+ theme: Resources.Theme? = null,
+ res: Resources,
+ resId: Int
+): AnimatedImageVector {
+ val parser = res.getXml(resId).seekToStartTag()
+ val attrs = Xml.asAttributeSet(parser)
+ return parser.parseAnimatedImageVector(res, theme, attrs)
+}
diff --git a/compose/animation/animation-graphics/src/androidMain/kotlin/androidx/compose/animation/graphics/res/AnimatorResources.android.kt b/compose/animation/animation-graphics/src/androidMain/kotlin/androidx/compose/animation/graphics/res/AnimatorResources.android.kt
new file mode 100644
index 0000000..ed925eb
--- /dev/null
+++ b/compose/animation/animation-graphics/src/androidMain/kotlin/androidx/compose/animation/graphics/res/AnimatorResources.android.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.animation.graphics.res
+
+import android.animation.TimeInterpolator
+import android.content.res.Resources
+import android.util.Xml
+import android.view.animation.AnticipateOvershootInterpolator
+import android.view.animation.BounceInterpolator
+import androidx.compose.animation.core.Easing
+import androidx.compose.animation.core.FastOutLinearInEasing
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.graphics.vector.compat.AndroidVectorResources
+import androidx.compose.animation.graphics.vector.Animator
+import androidx.compose.animation.graphics.vector.compat.TagObjectAnimator
+import androidx.compose.animation.graphics.vector.compat.TagSet
+import androidx.compose.animation.graphics.vector.compat.seekToStartTag
+import androidx.compose.animation.graphics.vector.compat.parseAnimatorSet
+import androidx.compose.animation.graphics.vector.compat.parseInterpolator
+import androidx.compose.animation.graphics.vector.compat.parseObjectAnimator
+import org.xmlpull.v1.XmlPullParserException
+import kotlin.math.PI
+import kotlin.math.cos
+import kotlin.math.pow
+import kotlin.math.sin
+
+/**
+ * Synchronously loads an [Animator] resource.
+ */
+@Throws(XmlPullParserException::class)
+internal fun loadAnimatorResource(
+ theme: Resources.Theme? = null,
+ res: Resources,
+ resId: Int
+): Animator {
+ val parser = res.getXml(resId)
+ val attrs = Xml.asAttributeSet(parser)
+
+ parser.seekToStartTag()
+ return when (parser.name) {
+ TagSet -> {
+ parser.parseAnimatorSet(res, theme, attrs)
+ }
+ TagObjectAnimator -> {
+ parser.parseObjectAnimator(res, theme, attrs)
+ }
+ else -> {
+ throw XmlPullParserException("Unknown tag: ${parser.name}")
+ }
+ }
+}
+
+internal fun TimeInterpolator.toEasing() = Easing { x -> getInterpolation(x) }
+
+internal val AccelerateDecelerateEasing = Easing { x ->
+ ((cos((x + 1) * Math.PI) / 2.0f) + 0.5f).toFloat()
+}
+
+internal val AccelerateEasing = Easing { x -> x * x }
+internal fun AccelerateEasing(factor: Float) = Easing { x -> x.pow(factor * 2) }
+
+internal fun AnticipateEasing(tension: Float) =
+ Easing { x -> x * x * ((tension + 1) * x - tension) }
+
+internal fun AnticipateOvershootEasing(tension: Float, extraTension: Float): Easing =
+ AnticipateOvershootInterpolator(tension, extraTension).toEasing()
+
+internal val BounceEasing: Easing = BounceInterpolator().toEasing()
+
+internal fun CycleEasing(cycle: Float) = Easing { x -> sin(2 * cycle * PI * x).toFloat() }
+
+internal val DecelerateEasing = Easing { x -> 1.0f - (1.0f - x) * (1.0f - x) }
+internal fun DecelerateEasing(factor: Float) = Easing { x -> 1.0f - (1.0f - x).pow(2 * factor) }
+
+internal fun OvershootEasing(tension: Float) =
+ Easing { x -> (x - 1f).let { t -> t * t * ((tension + 1f) * t + tension) + 1f } }
+
+private val builtinInterpolators = hashMapOf(
+ android.R.anim.linear_interpolator to LinearEasing,
+ android.R.interpolator.fast_out_linear_in to FastOutLinearInEasing,
+ android.R.interpolator.fast_out_slow_in to FastOutSlowInEasing,
+ android.R.interpolator.linear to LinearEasing,
+ android.R.interpolator.linear_out_slow_in to LinearOutSlowInEasing,
+ AndroidVectorResources.FAST_OUT_LINEAR_IN to FastOutLinearInEasing,
+ AndroidVectorResources.FAST_OUT_SLOW_IN to FastOutSlowInEasing,
+ AndroidVectorResources.LINEAR_OUT_SLOW_IN to LinearOutSlowInEasing
+)
+
+/**
+ * Synchronously loads an interpolator resource as an [Easing].
+ */
+@Throws(XmlPullParserException::class)
+internal fun loadInterpolatorResource(
+ theme: Resources.Theme? = null,
+ res: Resources,
+ resId: Int
+): Easing {
+ return builtinInterpolators[resId]
+ ?: res.getXml(resId).run {
+ seekToStartTag().parseInterpolator(res, theme, Xml.asAttributeSet(this))
+ }
+}
diff --git a/compose/animation/animation-graphics/src/androidMain/kotlin/androidx/compose/animation/graphics/vector/compat/AndroidVectorResources.android.kt b/compose/animation/animation-graphics/src/androidMain/kotlin/androidx/compose/animation/graphics/vector/compat/AndroidVectorResources.android.kt
new file mode 100644
index 0000000..b2b4521
--- /dev/null
+++ b/compose/animation/animation-graphics/src/androidMain/kotlin/androidx/compose/animation/graphics/vector/compat/AndroidVectorResources.android.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.animation.graphics.vector.compat
+
+/**
+ * Constants used to resolve AnimatedVectorDrawable attributes during xml inflation
+ */
+internal object AndroidVectorResources {
+
+ val STYLEABLE_ANIMATED_VECTOR_DRAWABLE = intArrayOf(android.R.attr.drawable)
+ const val STYLEABLE_ANIMATED_VECTOR_DRAWABLE_DRAWABLE = 0
+
+ val STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET =
+ intArrayOf(android.R.attr.name, android.R.attr.animation)
+ const val STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET_ANIMATION = 1
+ const val STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET_NAME = 0
+
+ val STYLEABLE_ANIMATOR = intArrayOf(
+ 0x01010141,
+ 0x01010198,
+ 0x010101be,
+ 0x010101bf,
+ 0x010101c0,
+ 0x010102de,
+ 0x010102df,
+ 0x010102e0
+ )
+ const val STYLEABLE_ANIMATOR_INTERPOLATOR = 0
+ const val STYLEABLE_ANIMATOR_DURATION = 1
+ const val STYLEABLE_ANIMATOR_START_OFFSET = 2
+ const val STYLEABLE_ANIMATOR_REPEAT_COUNT = 3
+ const val STYLEABLE_ANIMATOR_REPEAT_MODE = 4
+ const val STYLEABLE_ANIMATOR_VALUE_FROM = 5
+ const val STYLEABLE_ANIMATOR_VALUE_TO = 6
+ const val STYLEABLE_ANIMATOR_VALUE_TYPE = 7
+
+ val STYLEABLE_ANIMATOR_SET = intArrayOf(0x010102e2)
+ const val STYLEABLE_ANIMATOR_SET_ORDERING = 0
+
+ val STYLEABLE_PROPERTY_VALUES_HOLDER =
+ intArrayOf(0x010102de, 0x010102df, 0x010102e0, 0x010102e1)
+ const val STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_FROM = 0
+ const val STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_TO = 1
+ const val STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_TYPE = 2
+ const val STYLEABLE_PROPERTY_VALUES_HOLDER_PROPERTY_NAME = 3
+
+ val STYLEABLE_KEYFRAME = intArrayOf(0x01010024, 0x01010141, 0x010102e0, 0x010104d8)
+ const val STYLEABLE_KEYFRAME_VALUE = 0
+ const val STYLEABLE_KEYFRAME_INTERPOLATOR = 1
+ const val STYLEABLE_KEYFRAME_VALUE_TYPE = 2
+ const val STYLEABLE_KEYFRAME_FRACTION = 3
+
+ val STYLEABLE_PROPERTY_ANIMATOR = intArrayOf(0x010102e1, 0x01010405, 0x01010474, 0x01010475)
+ const val STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_NAME = 0
+ const val STYLEABLE_PROPERTY_ANIMATOR_PATH_DATA = 1
+ const val STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_X_NAME = 2
+ const val STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_Y_NAME = 3
+
+ val STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR = intArrayOf(
+ android.R.attr.tension,
+ android.R.attr.extraTension
+ )
+ const val STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR_TENSION = 0
+ const val STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR_EXTRA_TENSION = 1
+
+ val STYLEABLE_ACCELERATE_INTERPOLATOR = intArrayOf(android.R.attr.factor)
+ const val STYLEABLE_ACCELERATE_INTERPOLATOR_FACTOR = 0
+
+ val STYLEABLE_DECELERATE_INTERPOLATOR = intArrayOf(android.R.attr.factor)
+ const val STYLEABLE_DECELERATE_INTERPOLATOR_FACTOR = 0
+
+ val STYLEABLE_CYCLE_INTERPOLATOR = intArrayOf(android.R.attr.cycles)
+ const val STYLEABLE_CYCLE_INTERPOLATOR_CYCLES = 0
+
+ val STYLEABLE_OVERSHOOT_INTERPOLATOR = intArrayOf(android.R.attr.tension)
+ const val STYLEABLE_OVERSHOOT_INTERPOLATOR_TENSION = 0
+
+ val STYLEABLE_PATH_INTERPOLATOR =
+ intArrayOf(0x010103fc, 0x010103fd, 0x010103fe, 0x010103ff, 0x01010405)
+ const val STYLEABLE_PATH_INTERPOLATOR_CONTROL_X_1 = 0
+ const val STYLEABLE_PATH_INTERPOLATOR_CONTROL_Y_1 = 1
+ const val STYLEABLE_PATH_INTERPOLATOR_CONTROL_X_2 = 2
+ const val STYLEABLE_PATH_INTERPOLATOR_CONTROL_Y_2 = 3
+ const val STYLEABLE_PATH_INTERPOLATOR_PATH_DATA = 4
+
+ const val FAST_OUT_LINEAR_IN = 0x010c000f
+ const val FAST_OUT_SLOW_IN = 0x010c000d
+ const val LINEAR_OUT_SLOW_IN = 0x010c000e
+}
diff --git a/compose/animation/animation-graphics/src/androidMain/kotlin/androidx/compose/animation/graphics/vector/compat/XmlAnimatedVectorParser.android.kt b/compose/animation/animation-graphics/src/androidMain/kotlin/androidx/compose/animation/graphics/vector/compat/XmlAnimatedVectorParser.android.kt
new file mode 100644
index 0000000..e1cc77e
--- /dev/null
+++ b/compose/animation/animation-graphics/src/androidMain/kotlin/androidx/compose/animation/graphics/vector/compat/XmlAnimatedVectorParser.android.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.animation.graphics.vector.compat
+
+import android.content.res.Resources
+import android.util.AttributeSet
+import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
+import androidx.compose.animation.graphics.res.loadAnimatorResource
+import androidx.compose.animation.graphics.vector.AnimatedImageVector
+import androidx.compose.animation.graphics.vector.AnimatedVectorTarget
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.vectorResource
+import org.xmlpull.v1.XmlPullParser
+
+private const val TagAnimatedVector = "animated-vector"
+private const val TagAnimatedVectorTarget = "target"
+
+private fun parseAnimatedVectorTarget(
+ res: Resources,
+ theme: Resources.Theme?,
+ attrs: AttributeSet
+): AnimatedVectorTarget {
+ return attrs.attrs(
+ res, theme, AndroidVectorResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET
+ ) { a ->
+ AnimatedVectorTarget(
+ a.getString(
+ AndroidVectorResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET_NAME
+ ) ?: "",
+ loadAnimatorResource(
+ theme,
+ res,
+ a.getResourceId(
+ AndroidVectorResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET_ANIMATION,
+ 0
+ )
+ )
+ )
+ }
+}
+
+@ExperimentalAnimationGraphicsApi
+internal fun XmlPullParser.parseAnimatedImageVector(
+ res: Resources,
+ theme: Resources.Theme?,
+ attrs: AttributeSet
+): AnimatedImageVector {
+ return attrs.attrs(res, theme, AndroidVectorResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE) { a ->
+ val drawableId = a.getResourceId(
+ AndroidVectorResources.STYLEABLE_ANIMATED_VECTOR_DRAWABLE_DRAWABLE,
+ 0
+ )
+ val targets = mutableListOf<AnimatedVectorTarget>()
+ forEachChildOf(TagAnimatedVector) {
+ if (eventType == XmlPullParser.START_TAG && name == TagAnimatedVectorTarget) {
+ targets.add(parseAnimatedVectorTarget(res, theme, attrs))
+ }
+ }
+ AnimatedImageVector(
+ ImageVector.vectorResource(theme, res, drawableId),
+ targets
+ )
+ }
+}
diff --git a/compose/animation/animation-graphics/src/androidMain/kotlin/androidx/compose/animation/graphics/vector/compat/XmlAnimatorParser.android.kt b/compose/animation/animation-graphics/src/androidMain/kotlin/androidx/compose/animation/graphics/vector/compat/XmlAnimatorParser.android.kt
new file mode 100644
index 0000000..e6016d2
--- /dev/null
+++ b/compose/animation/animation-graphics/src/androidMain/kotlin/androidx/compose/animation/graphics/vector/compat/XmlAnimatorParser.android.kt
@@ -0,0 +1,521 @@
+/*
+ * 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.animation.graphics.vector.compat
+
+import android.content.res.Resources
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import android.util.TypedValue
+import android.view.animation.PathInterpolator
+import androidx.compose.animation.core.CubicBezierEasing
+import androidx.compose.animation.core.Easing
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.graphics.res.AccelerateDecelerateEasing
+import androidx.compose.animation.graphics.res.AccelerateEasing
+import androidx.compose.animation.graphics.res.AnticipateEasing
+import androidx.compose.animation.graphics.res.AnticipateOvershootEasing
+import androidx.compose.animation.graphics.res.BounceEasing
+import androidx.compose.animation.graphics.res.CycleEasing
+import androidx.compose.animation.graphics.res.DecelerateEasing
+import androidx.compose.animation.graphics.res.OvershootEasing
+import androidx.compose.animation.graphics.res.loadInterpolatorResource
+import androidx.compose.animation.graphics.res.toEasing
+import androidx.compose.animation.graphics.vector.Animator
+import androidx.compose.animation.graphics.vector.AnimatorSet
+import androidx.compose.animation.graphics.vector.Keyframe
+import androidx.compose.animation.graphics.vector.ObjectAnimator
+import androidx.compose.animation.graphics.vector.Ordering
+import androidx.compose.animation.graphics.vector.PropertyValuesHolder
+import androidx.compose.animation.graphics.vector.PropertyValuesHolder1D
+import androidx.compose.animation.graphics.vector.PropertyValuesHolder2D
+import androidx.compose.animation.graphics.vector.PropertyValuesHolderColor
+import androidx.compose.animation.graphics.vector.PropertyValuesHolderFloat
+import androidx.compose.animation.graphics.vector.PropertyValuesHolderInt
+import androidx.compose.animation.graphics.vector.PropertyValuesHolderPath
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.PathNode
+import androidx.compose.ui.graphics.vector.addPathNodes
+import androidx.core.graphics.PathParser
+import org.xmlpull.v1.XmlPullParser
+
+internal const val TagSet = "set"
+internal const val TagObjectAnimator = "objectAnimator"
+private const val TagPropertyValuesHolder = "propertyValuesHolder"
+private const val TagKeyframe = "keyframe"
+
+private const val ValueTypeFloat = 0
+private const val ValueTypeInt = 1
+private const val ValueTypePath = 2
+private const val ValueTypeColor = 3
+private const val ValueTypeUndefined = 4
+
+private const val RepeatModeReverse = 2
+
+private enum class ValueType {
+ Float,
+ Int,
+ Color,
+ Path
+}
+
+private val FallbackValueType = ValueType.Float
+
+private fun TypedArray.getInterpolator(
+ res: Resources,
+ theme: Resources.Theme?,
+ index: Int,
+ defaultValue: Easing
+): Easing {
+ val id = getResourceId(index, 0)
+ return if (id == 0) {
+ defaultValue
+ } else {
+ loadInterpolatorResource(theme, res, id)
+ }
+}
+
+private fun parseKeyframe(
+ res: Resources,
+ theme: Resources.Theme?,
+ attrs: AttributeSet,
+ holderValueType: ValueType?,
+ defaultInterpolator: Easing
+): Pair<Keyframe<Any>, ValueType> {
+ return attrs.attrs(res, theme, AndroidVectorResources.STYLEABLE_KEYFRAME) { a ->
+ val inferredValueType =
+ // The type is specified in <propertyValuesHolder>.
+ holderValueType
+ ?: inferValueType( // Identify the type from our attribute values.
+ a.getInt(
+ AndroidVectorResources.STYLEABLE_KEYFRAME_VALUE_TYPE,
+ ValueTypeUndefined
+ ),
+ a.peekValue(AndroidVectorResources.STYLEABLE_KEYFRAME_VALUE).type
+ )
+ // We didn't have any clue until the end.
+ ?: FallbackValueType
+ a.getKeyframe(
+ a.getFloat(AndroidVectorResources.STYLEABLE_KEYFRAME_FRACTION, 0f),
+ a.getInterpolator(
+ res,
+ theme,
+ AndroidVectorResources.STYLEABLE_KEYFRAME_INTERPOLATOR,
+ defaultInterpolator
+ ),
+ inferredValueType,
+ AndroidVectorResources.STYLEABLE_KEYFRAME_VALUE
+ ) to inferredValueType // Report back the type to <propertyValuesHolder>.
+ }
+}
+
+/**
+ * Extracts a [Keyframe] value from this [TypedArray]. This [TypedArray] can come from either
+ * `<propertyValuesHolder>` or `<keyframe>`
+ */
+private fun TypedArray.getKeyframe(
+ fraction: Float,
+ interpolator: Easing,
+ valueType: ValueType,
+ valueIndex: Int
+): Keyframe<Any> {
+ return when (valueType) {
+ ValueType.Float -> Keyframe(
+ fraction,
+ getFloat(valueIndex, 0f),
+ interpolator
+ )
+ ValueType.Int -> Keyframe(
+ fraction,
+ getInt(valueIndex, 0),
+ interpolator
+ )
+ ValueType.Color -> Keyframe(
+ fraction,
+ Color(getColor(valueIndex, 0)),
+ interpolator
+ )
+ ValueType.Path -> Keyframe(
+ fraction,
+ addPathNodes(getString(valueIndex)),
+ interpolator
+ )
+ }
+}
+
+private fun XmlPullParser.parsePropertyValuesHolder(
+ res: Resources,
+ theme: Resources.Theme?,
+ attrs: AttributeSet,
+ interpolator: Easing
+): PropertyValuesHolder<*> {
+ return attrs.attrs(
+ res,
+ theme,
+ AndroidVectorResources.STYLEABLE_PROPERTY_VALUES_HOLDER
+ ) { a ->
+ a.getPropertyValuesHolder1D(
+ a.getString(
+ AndroidVectorResources.STYLEABLE_PROPERTY_VALUES_HOLDER_PROPERTY_NAME
+ )!!,
+ AndroidVectorResources.STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_TYPE,
+ AndroidVectorResources.STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_FROM,
+ AndroidVectorResources.STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_TO,
+ interpolator
+ ) { valueType, keyframes ->
+ var vt: ValueType? = null
+ forEachChildOf(TagPropertyValuesHolder) {
+ if (eventType == XmlPullParser.START_TAG && name == TagKeyframe) {
+ val (keyframe, keyframeValueType) =
+ parseKeyframe(res, theme, attrs, valueType, interpolator)
+ if (vt == null) vt = keyframeValueType
+ keyframes.add(keyframe)
+ }
+ }
+ // This determines the final ValueType of the PropertyValuesHolder.
+ vt ?: valueType ?: FallbackValueType
+ }
+ }
+}
+
+/**
+ * Infers a [ValueType] from various information from XML.
+ *
+ * @param valueType The `valueType` attribute specified in the XML.
+ * @param typedValueTypes [TypedValue.type] values taken from multiple [TypedValue]s.
+ * @return A [ValueType] identified by the information so far, or `null` if it is uncertain.
+ */
+private fun inferValueType(valueType: Int, vararg typedValueTypes: Int): ValueType? {
+ return when (valueType) {
+ ValueTypeFloat -> ValueType.Float
+ ValueTypeInt -> ValueType.Int
+ ValueTypeColor -> ValueType.Color
+ ValueTypePath -> ValueType.Path
+ else ->
+ if (
+ typedValueTypes
+ .all {
+ it in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT
+ }
+ ) {
+ ValueType.Color
+ } else {
+ null
+ }
+ }
+}
+
+/**
+ * Extracts attribute values related to [PropertyValuesHolder]. This [TypedArray] can be taken from
+ * either `<objectAnimator>` or `<propertyValuesHolder>`.
+ *
+ * @param parseKeyframes The caller should parse `<keyframe>`s inside of this
+ * `<propertyValuesHolder>` and store them in the `keyframes` [MutableList]. The lambda receives
+ * a [ValueType] if it has been identified so far. The lambda has to return [ValueType] in case it
+ * is first identified while parsing keyframes.
+ */
+private fun TypedArray.getPropertyValuesHolder1D(
+ propertyName: String,
+ valueTypeIndex: Int,
+ valueFromIndex: Int,
+ valueToIndex: Int,
+ interpolator: Easing,
+ parseKeyframes: (
+ valueType: ValueType?,
+ keyframes: MutableList<Keyframe<Any>>
+ ) -> ValueType = { vt, _ -> vt ?: FallbackValueType }
+): PropertyValuesHolder1D<*> {
+ val valueType = getInt(
+ valueTypeIndex,
+ ValueTypeUndefined
+ )
+
+ val valueFrom = peekValue(valueFromIndex)
+ val hasFrom = valueFrom != null
+ val typeFrom = valueFrom?.type ?: ValueTypeUndefined
+
+ val valueTo = peekValue(valueToIndex)
+ val hasTo = valueTo != null
+ val typeTo = valueTo?.type ?: ValueTypeUndefined
+
+ var inferredValueType =
+ inferValueType(
+ valueType,
+ typeFrom,
+ typeTo
+ )
+ val keyframes = mutableListOf<Keyframe<Any>>()
+ if (inferredValueType == null && (hasFrom || hasTo)) {
+ inferredValueType =
+ ValueType.Float
+ }
+ if (hasFrom) {
+ keyframes.add(getKeyframe(0f, interpolator, inferredValueType!!, valueFromIndex))
+ }
+ if (hasTo) {
+ keyframes.add(getKeyframe(1f, interpolator, inferredValueType!!, valueToIndex))
+ }
+ inferredValueType = parseKeyframes(inferredValueType, keyframes)
+ keyframes.sortBy { it.fraction }
+ @Suppress("UNCHECKED_CAST")
+ return when (inferredValueType) {
+ ValueType.Float -> PropertyValuesHolderFloat(
+ propertyName,
+ keyframes as List<Keyframe<Float>>
+ )
+ ValueType.Int -> PropertyValuesHolderInt(
+ propertyName,
+ keyframes as List<Keyframe<Int>>
+ )
+ ValueType.Color -> PropertyValuesHolderColor(
+ propertyName,
+ keyframes as List<Keyframe<Color>>
+ )
+ ValueType.Path -> PropertyValuesHolderPath(
+ propertyName,
+ keyframes as List<Keyframe<List<PathNode>>>
+ )
+ }
+}
+
+private fun convertRepeatMode(repeatMode: Int) = when (repeatMode) {
+ RepeatModeReverse -> RepeatMode.Reverse
+ else -> RepeatMode.Restart
+}
+
+internal fun XmlPullParser.parseObjectAnimator(
+ res: Resources,
+ theme: Resources.Theme?,
+ attrs: AttributeSet
+): ObjectAnimator {
+ return attrs.attrs(res, theme, AndroidVectorResources.STYLEABLE_ANIMATOR) { a ->
+ attrs.attrs(res, theme, AndroidVectorResources.STYLEABLE_PROPERTY_ANIMATOR) { oa ->
+ val interpolator = a.getInterpolator(
+ res,
+ theme,
+ AndroidVectorResources.STYLEABLE_ANIMATOR_INTERPOLATOR,
+ AccelerateDecelerateEasing
+ )
+ val holders = mutableListOf<PropertyValuesHolder<*>>()
+ // 2D; This <objectAnimator> has `propertyXName`, `propertyYName`, and `pathData`.
+ oa.getString(
+ AndroidVectorResources.STYLEABLE_PROPERTY_ANIMATOR_PATH_DATA
+ )?.let { pathData ->
+ holders.add(
+ PropertyValuesHolder2D(
+ oa.getString(
+ AndroidVectorResources.STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_X_NAME
+ )!!,
+ oa.getString(
+ AndroidVectorResources.STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_Y_NAME
+ )!!,
+ addPathNodes(pathData),
+ interpolator
+ )
+ )
+ }
+ // 1D; This <objectAnimator> has `propertyName`, `valueFrom`, and `valueTo`.
+ oa.getString(
+ AndroidVectorResources.STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_NAME
+ )?.let { propertyName ->
+ holders.add(
+ a.getPropertyValuesHolder1D(
+ propertyName,
+ AndroidVectorResources.STYLEABLE_ANIMATOR_VALUE_TYPE,
+ AndroidVectorResources.STYLEABLE_ANIMATOR_VALUE_FROM,
+ AndroidVectorResources.STYLEABLE_ANIMATOR_VALUE_TO,
+ interpolator
+ )
+ )
+ }
+ // This <objectAnimator> has <propertyValuesHolder> inside.
+ forEachChildOf(TagObjectAnimator) {
+ if (eventType == XmlPullParser.START_TAG && name == TagPropertyValuesHolder) {
+ holders.add(parsePropertyValuesHolder(res, theme, attrs, interpolator))
+ }
+ }
+
+ ObjectAnimator(
+ duration = a.getInt(
+ AndroidVectorResources.STYLEABLE_ANIMATOR_DURATION,
+ 300
+ ),
+ startDelay = a.getInt(
+ AndroidVectorResources.STYLEABLE_ANIMATOR_START_OFFSET,
+ 0
+ ),
+ repeatCount = a.getInt(
+ AndroidVectorResources.STYLEABLE_ANIMATOR_REPEAT_COUNT,
+ 0
+ ),
+ repeatMode = convertRepeatMode(
+ a.getInt(AndroidVectorResources.STYLEABLE_ANIMATOR_REPEAT_MODE, 0)
+ ),
+ holders = holders
+ )
+ }
+ }
+}
+
+internal fun XmlPullParser.parseAnimatorSet(
+ res: Resources,
+ theme: Resources.Theme?,
+ attrs: AttributeSet
+): AnimatorSet {
+ return attrs.attrs(res, theme, AndroidVectorResources.STYLEABLE_ANIMATOR_SET) { a ->
+ val ordering = a.getInt(AndroidVectorResources.STYLEABLE_ANIMATOR_SET_ORDERING, 0)
+ val animators = mutableListOf<Animator>()
+ forEachChildOf(TagSet) {
+ if (eventType == XmlPullParser.START_TAG) {
+ when (name) {
+ TagSet -> animators.add(parseAnimatorSet(res, theme, attrs))
+ TagObjectAnimator -> animators.add(parseObjectAnimator(res, theme, attrs))
+ }
+ }
+ }
+ AnimatorSet(
+ animators,
+ if (ordering != 0) Ordering.Sequentially else Ordering.Together
+ )
+ }
+}
+
+internal fun XmlPullParser.parseInterpolator(
+ res: Resources,
+ theme: Resources.Theme?,
+ attrs: AttributeSet
+): Easing {
+ return when (name) {
+ "linearInterpolator" -> LinearEasing
+ "accelerateInterpolator" ->
+ attrs.attrs(
+ res, theme, AndroidVectorResources.STYLEABLE_ACCELERATE_INTERPOLATOR
+ ) { a ->
+ val factor = a.getFloat(
+ AndroidVectorResources.STYLEABLE_ACCELERATE_INTERPOLATOR_FACTOR, 1.0f
+ )
+ if (factor == 1.0f) AccelerateEasing else AccelerateEasing(factor)
+ }
+ "decelerateInterpolator" ->
+ attrs.attrs(
+ res, theme, AndroidVectorResources.STYLEABLE_DECELERATE_INTERPOLATOR
+ ) { a ->
+ val factor = a.getFloat(
+ AndroidVectorResources.STYLEABLE_DECELERATE_INTERPOLATOR_FACTOR, 1.0f
+ )
+ if (factor == 1.0f) DecelerateEasing else DecelerateEasing(factor)
+ }
+ "accelerateDecelerateInterpolator" -> AccelerateDecelerateEasing
+ "cycleInterpolator" ->
+ attrs.attrs(
+ res, theme, AndroidVectorResources.STYLEABLE_CYCLE_INTERPOLATOR
+ ) { a ->
+ CycleEasing(
+ a.getFloat(
+ AndroidVectorResources.STYLEABLE_CYCLE_INTERPOLATOR_CYCLES, 1.0f
+ )
+ )
+ }
+ "anticipateInterpolator" ->
+ attrs.attrs(
+ res,
+ theme,
+ AndroidVectorResources.STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR
+ ) { a ->
+ AnticipateEasing(
+ a.getFloat(
+ AndroidVectorResources.STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR_TENSION,
+ 2.0f
+ )
+ )
+ }
+ "overshootInterpolator" ->
+ attrs.attrs(
+ res, theme, AndroidVectorResources.STYLEABLE_OVERSHOOT_INTERPOLATOR
+ ) { a ->
+ OvershootEasing(
+ a.getFloat(
+ AndroidVectorResources.STYLEABLE_OVERSHOOT_INTERPOLATOR_TENSION,
+ 2.0f
+ )
+ )
+ }
+ "anticipateOvershootInterpolator" ->
+ attrs.attrs(
+ res,
+ theme,
+ AndroidVectorResources.STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR
+ ) { a ->
+ AnticipateOvershootEasing(
+ a.getFloat(
+ AndroidVectorResources.STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR_TENSION,
+ 2.0f
+ ),
+ a.getFloat(
+ AndroidVectorResources
+ .STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR_EXTRA_TENSION,
+ 1.5f
+ )
+ )
+ }
+ "bounceInterpolator" -> BounceEasing
+ "pathInterpolator" ->
+ attrs.attrs(
+ res, theme, AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR
+ ) { a ->
+ val pathData =
+ a.getString(AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_PATH_DATA)
+ if (pathData != null) {
+ PathInterpolator(PathParser.createPathFromPathData(pathData)).toEasing()
+ } else if (
+ !a.hasValue(AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_X_2) ||
+ !a.hasValue(AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_Y_2)
+ ) {
+ PathInterpolator(
+ a.getFloat(
+ AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_X_1,
+ 0f
+ ),
+ a.getFloat(
+ AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_Y_1,
+ 0f
+ )
+ ).toEasing()
+ } else {
+ CubicBezierEasing(
+ a.getFloat(
+ AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_X_1,
+ 0f
+ ),
+ a.getFloat(
+ AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_Y_1,
+ 0f
+ ),
+ a.getFloat(
+ AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_X_2,
+ 1f
+ ),
+ a.getFloat(
+ AndroidVectorResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_Y_2,
+ 1f
+ )
+ )
+ }
+ }
+ else -> throw RuntimeException("Unknown interpolator: $name")
+ }
+}
diff --git a/compose/animation/animation-graphics/src/androidMain/kotlin/androidx/compose/animation/graphics/vector/compat/XmlPullParserUtils.android.kt b/compose/animation/animation-graphics/src/androidMain/kotlin/androidx/compose/animation/graphics/vector/compat/XmlPullParserUtils.android.kt
new file mode 100644
index 0000000..ab35418
--- /dev/null
+++ b/compose/animation/animation-graphics/src/androidMain/kotlin/androidx/compose/animation/graphics/vector/compat/XmlPullParserUtils.android.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.animation.graphics.vector.compat
+
+import android.content.res.Resources
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import org.xmlpull.v1.XmlPullParser
+import org.xmlpull.v1.XmlPullParserException
+
+internal fun XmlPullParser.isAtEnd(): Boolean =
+ eventType == XmlPullParser.END_DOCUMENT ||
+ (depth < 1 && eventType == XmlPullParser.END_TAG)
+
+/**
+ * Helper method to seek to the first tag within the VectorDrawable xml asset
+ */
+@Throws(XmlPullParserException::class)
+internal fun XmlPullParser.seekToStartTag(): XmlPullParser {
+ var type = next()
+ while (type != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) {
+ // Empty loop
+ type = next()
+ }
+ if (type != XmlPullParser.START_TAG) {
+ throw XmlPullParserException("No start tag found")
+ }
+ return this
+}
+
+/**
+ * Assuming that we are at the [XmlPullParser.START_TAG start] of the specified [tag], iterates
+ * though the events until we see a corresponding [XmlPullParser.END_TAG].
+ */
+internal inline fun XmlPullParser.forEachChildOf(
+ tag: String,
+ action: XmlPullParser.() -> Unit
+) {
+ next()
+ while (!isAtEnd()) {
+ if (eventType == XmlPullParser.END_TAG && name == tag) {
+ break
+ }
+ action()
+ next()
+ }
+}
+
+internal inline fun <T> AttributeSet.attrs(
+ res: Resources,
+ theme: Resources.Theme?,
+ styleable: IntArray,
+ body: (a: TypedArray) -> T
+): T {
+ val a = theme?.obtainStyledAttributes(this, styleable, 0, 0)
+ ?: res.obtainAttributes(this, styleable)
+ try {
+ return body(a)
+ } finally {
+ a.recycle()
+ }
+}
diff --git a/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt b/compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/ExperimentalAnimationGraphicsApi.kt
similarity index 64%
copy from benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt
copy to compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/ExperimentalAnimationGraphicsApi.kt
index 88234c7..0dc6ac5 100644
--- a/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt
+++ b/compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/ExperimentalAnimationGraphicsApi.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,8 @@
* limitations under the License.
*/
-package androidx.benchmark.macro
+package androidx.compose.animation.graphics
-import android.platform.test.rule.DropCachesRule
-
-class BenchmarkClass {
- val rule = DropCachesRule()
-}
+@RequiresOptIn(message = "This is an experimental animation graphics API.")
+@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
+annotation class ExperimentalAnimationGraphicsApi
diff --git a/compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/vector/AnimatedImageVector.kt b/compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/vector/AnimatedImageVector.kt
new file mode 100644
index 0000000..c688f19
--- /dev/null
+++ b/compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/vector/AnimatedImageVector.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.animation.graphics.vector
+
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.RenderVectorGroup
+import androidx.compose.ui.graphics.vector.VectorGroup
+import androidx.compose.ui.graphics.vector.VectorConfig
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.util.fastMaxBy
+
+/**
+ * Animated vector graphics object that is generated as a result of
+ * [androidx.compose.animation.graphics.res.loadAnimatedVectorResource].
+ * It can be composed and rendered by [painterFor].
+ *
+ * @param imageVector The [ImageVector] to be animated. This is represented with the
+ * `android:drawable` parameter of an `<animated-vector>` element.
+ */
+@ExperimentalAnimationGraphicsApi
+@Immutable
+class AnimatedImageVector internal constructor(
+ val imageVector: ImageVector,
+ // The list of [AnimatedVectorTarget]s that specify animations for each of the elements in the
+ // drawable. This is represented with `<target>` elements in `<animated-vector>`. This list is
+ // expected to be *immutable*.
+ internal val targets: List<AnimatedVectorTarget>
+) {
+
+ /**
+ * The total duration of all the animations in this image, including start delays and repeats.
+ */
+ val totalDuration = targets.fastMaxBy {
+ it.animator.totalDuration
+ }?.animator?.totalDuration ?: 0
+
+ /**
+ * Creates and remembers a [Painter] to render this [AnimatedImageVector]. It renders the image
+ * either at the start or the end of all the animations depending on the [atEnd]. Changes to
+ * [atEnd] are animated.
+ *
+ * @param atEnd Whether the animated vector should be rendered at the end of all its animations.
+ *
+ * @sample androidx.compose.animation.graphics.samples.AnimatedVectorSample
+ */
+ @OptIn(ExperimentalComposeUiApi::class)
+ @Composable
+ fun painterFor(atEnd: Boolean): Painter {
+ return painterFor(atEnd) { group, overrides ->
+ RenderVectorGroup(group, overrides)
+ }
+ }
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ @Composable
+ internal fun painterFor(
+ atEnd: Boolean,
+ render: @Composable (VectorGroup, Map<String, VectorConfig>) -> Unit
+ ): Painter {
+ return rememberVectorPainter(
+ defaultWidth = imageVector.defaultWidth,
+ defaultHeight = imageVector.defaultHeight,
+ viewportWidth = imageVector.viewportWidth,
+ viewportHeight = imageVector.viewportHeight,
+ name = imageVector.name,
+ tintColor = imageVector.tintColor,
+ tintBlendMode = imageVector.tintBlendMode,
+ ) { _, _ ->
+ val transition = updateTransition(atEnd, label = imageVector.name)
+ render(
+ imageVector.root,
+ targets.associate { target ->
+ target.name to target.animator.createVectorConfig(transition, totalDuration)
+ }
+ )
+ }
+ }
+}
+
+/**
+ * Definition of animation to one of the elements in a [AnimatedImageVector].
+ */
+@Immutable
+internal class AnimatedVectorTarget(
+ val name: String,
+ val animator: Animator
+)
diff --git a/compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/vector/Animator.kt b/compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/vector/Animator.kt
new file mode 100644
index 0000000..5eb3680
--- /dev/null
+++ b/compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/vector/Animator.kt
@@ -0,0 +1,581 @@
+/*
+ * 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.animation.graphics.vector
+
+import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.Easing
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.KeyframesSpec
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.keyframes
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.vector.PathNode
+import androidx.compose.ui.graphics.vector.VectorConfig
+import androidx.compose.ui.graphics.vector.VectorProperty
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastMaxBy
+import androidx.compose.ui.util.fastSumBy
+import androidx.compose.ui.util.lerp
+
+internal sealed class Animator {
+ abstract val totalDuration: Int
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ @Composable
+ fun createVectorConfig(
+ transition: Transition<Boolean>,
+ overallDuration: Int
+ ): VectorConfig {
+ return StateVectorConfig().also { override ->
+ Configure(transition, override, overallDuration, 0)
+ }
+ }
+
+ @Composable
+ abstract fun Configure(
+ transition: Transition<Boolean>,
+ config: StateVectorConfig,
+ overallDuration: Int,
+ parentDelay: Int
+ )
+}
+
+internal data class ObjectAnimator(
+ val duration: Int,
+ val startDelay: Int,
+ val repeatCount: Int,
+ val repeatMode: RepeatMode,
+ val holders: List<PropertyValuesHolder<*>>
+) : Animator() {
+
+ override val totalDuration = if (repeatCount == Int.MAX_VALUE) {
+ Int.MAX_VALUE
+ } else {
+ startDelay + duration * (repeatCount + 1)
+ }
+
+ @Composable
+ override fun Configure(
+ transition: Transition<Boolean>,
+ config: StateVectorConfig,
+ overallDuration: Int,
+ parentDelay: Int
+ ) {
+ holders.fastForEach { holder ->
+ holder.AnimateIn(
+ config,
+ transition,
+ overallDuration,
+ duration,
+ parentDelay + startDelay
+ )
+ }
+ }
+}
+
+internal data class AnimatorSet(
+ val animators: List<Animator>,
+ val ordering: Ordering
+) : Animator() {
+
+ override val totalDuration = when (ordering) {
+ Ordering.Together -> animators.fastMaxBy { it.totalDuration }?.totalDuration ?: 0
+ Ordering.Sequentially -> animators.fastSumBy { it.totalDuration }
+ }
+
+ @Composable
+ override fun Configure(
+ transition: Transition<Boolean>,
+ config: StateVectorConfig,
+ overallDuration: Int,
+ parentDelay: Int
+ ) {
+ when (ordering) {
+ Ordering.Together -> {
+ animators.fastForEach { animator ->
+ animator.Configure(transition, config, overallDuration, parentDelay)
+ }
+ }
+ Ordering.Sequentially -> {
+ var accumulatedDelay = parentDelay
+ animators.fastForEach { animator ->
+ animator.Configure(transition, config, overallDuration, accumulatedDelay)
+ accumulatedDelay += animator.totalDuration
+ }
+ }
+ }
+ }
+}
+
+internal sealed class PropertyValuesHolder<T> {
+
+ @Composable
+ abstract fun AnimateIn(
+ config: StateVectorConfig,
+ transition: Transition<Boolean>,
+ overallDuration: Int,
+ duration: Int,
+ delay: Int
+ )
+}
+
+internal data class PropertyValuesHolder2D(
+ val xPropertyName: String,
+ val yPropertyName: String,
+ val pathData: List<PathNode>,
+ val interpolator: Easing
+) : PropertyValuesHolder<Pair<Float, Float>>() {
+
+ @Composable
+ override fun AnimateIn(
+ config: StateVectorConfig,
+ transition: Transition<Boolean>,
+ overallDuration: Int,
+ duration: Int,
+ delay: Int
+ ) {
+ // TODO(b/178978971): Implement path animation.
+ }
+}
+
+internal sealed class PropertyValuesHolder1D<T>(
+ val propertyName: String
+) : PropertyValuesHolder<T>() {
+
+ abstract val animatorKeyframes: List<Keyframe<T>>
+
+ protected val targetValueByState: @Composable (Boolean) -> T = { atEnd ->
+ if (atEnd) {
+ animatorKeyframes.last().value
+ } else {
+ animatorKeyframes.first().value
+ }
+ }
+
+ protected fun <R> createTransitionSpec(
+ overallDuration: Int,
+ duration: Int,
+ delay: Int,
+ addKeyframe: KeyframesSpec.KeyframesSpecConfig<R>.(
+ keyframe: Keyframe<T>,
+ time: Int,
+ easing: Easing
+ ) -> Unit
+ ): @Composable Transition.Segment<Boolean>.() -> FiniteAnimationSpec<R> {
+ return {
+ if (targetState) { // at end
+ keyframes {
+ durationMillis = duration
+ delayMillis = delay
+ animatorKeyframes.fastForEach { keyframe ->
+ val time = (duration * keyframe.fraction).toInt()
+ addKeyframe(keyframe, time, keyframe.interpolator)
+ }
+ }
+ } else {
+ keyframes {
+ durationMillis = duration
+ delayMillis = overallDuration - duration - delay
+ animatorKeyframes.fastForEach { keyframe ->
+ val time = (duration * (1 - keyframe.fraction)).toInt()
+ addKeyframe(keyframe, time, keyframe.interpolator.transpose())
+ }
+ }
+ }
+ }
+ }
+}
+
+internal class PropertyValuesHolderFloat(
+ propertyName: String,
+ override val animatorKeyframes: List<Keyframe<Float>>
+) : PropertyValuesHolder1D<Float>(propertyName) {
+
+ @Composable
+ override fun AnimateIn(
+ config: StateVectorConfig,
+ transition: Transition<Boolean>,
+ overallDuration: Int,
+ duration: Int,
+ delay: Int
+ ) {
+ val state = transition.animateFloat(
+ transitionSpec = createTransitionSpec(
+ overallDuration,
+ duration,
+ delay
+ ) { keyframe, time, easing ->
+ keyframe.value at time with easing
+ },
+ label = propertyName,
+ targetValueByState = targetValueByState
+ )
+ when (propertyName) {
+ "rotation" -> config.rotationState = state
+ "pivotX" -> config.pivotXState = state
+ "pivotY" -> config.pivotYState = state
+ "scaleX" -> config.scaleXState = state
+ "scaleY" -> config.scaleYState = state
+ "translateX" -> config.translateXState = state
+ "translateY" -> config.translateYState = state
+ "fillAlpha" -> config.fillAlphaState = state
+ "strokeWidth" -> config.strokeWidthState = state
+ "strokeAlpha" -> config.strokeAlphaState = state
+ "trimPathStart" -> config.trimPathStartState = state
+ "trimPathEnd" -> config.trimPathEndState = state
+ "trimPathOffset" -> config.trimPathOffsetState = state
+ else -> throw IllegalStateException("Unknown propertyName: $propertyName")
+ }
+ }
+}
+
+internal class PropertyValuesHolderInt(
+ propertyName: String,
+ override val animatorKeyframes: List<Keyframe<Int>>
+) : PropertyValuesHolder1D<Int>(propertyName) {
+
+ @Composable
+ override fun AnimateIn(
+ config: StateVectorConfig,
+ transition: Transition<Boolean>,
+ overallDuration: Int,
+ duration: Int,
+ delay: Int
+ ) {
+ // AnimatedVectorDrawable does not have an Int property; Ignore.
+ }
+}
+
+internal class PropertyValuesHolderColor(
+ propertyName: String,
+ override val animatorKeyframes: List<Keyframe<Color>>
+) : PropertyValuesHolder1D<Color>(propertyName) {
+
+ @Composable
+ override fun AnimateIn(
+ config: StateVectorConfig,
+ transition: Transition<Boolean>,
+ overallDuration: Int,
+ duration: Int,
+ delay: Int
+ ) {
+ val state = transition.animateColor(
+ transitionSpec = createTransitionSpec(
+ overallDuration,
+ duration,
+ delay
+ ) { keyframe, time, easing ->
+ keyframe.value at time with easing
+ },
+ label = propertyName,
+ targetValueByState = targetValueByState
+ )
+ when (propertyName) {
+ "fillColor" -> config.fillColorState = state
+ "strokeColor" -> config.strokeColorState = state
+ else -> throw IllegalStateException("Unknown propertyName: $propertyName")
+ }
+ }
+}
+
+internal class PropertyValuesHolderPath(
+ propertyName: String,
+ override val animatorKeyframes: List<Keyframe<List<PathNode>>>
+) : PropertyValuesHolder1D<List<PathNode>>(propertyName) {
+
+ @Composable
+ override fun AnimateIn(
+ config: StateVectorConfig,
+ transition: Transition<Boolean>,
+ overallDuration: Int,
+ duration: Int,
+ delay: Int
+ ) {
+ if (propertyName == "pathData") {
+ val state = transition.animateFloat(
+ transitionSpec = createTransitionSpec(
+ overallDuration,
+ duration,
+ delay
+ ) { keyframe, time, easing ->
+ keyframe.fraction at time with easing
+ },
+ label = propertyName
+ ) { atEnd ->
+ if (atEnd) {
+ animatorKeyframes.last().fraction
+ } else {
+ animatorKeyframes.first().fraction
+ }
+ }
+ config.pathDataStatePair = state to this
+ } else {
+ throw IllegalStateException("Unknown propertyName: $propertyName")
+ }
+ }
+
+ fun interpolate(fraction: Float): List<PathNode> {
+ val index = (animatorKeyframes.indexOfFirst { it.fraction > fraction } - 1).coerceAtLeast(0)
+ val easing = animatorKeyframes[index + 1].interpolator
+ val innerFraction = easing.transform(
+ (
+ (fraction - animatorKeyframes[index].fraction) /
+ (animatorKeyframes[index + 1].fraction - animatorKeyframes[index].fraction)
+ )
+ .coerceIn(0f, 1f)
+ )
+ return lerp(
+ animatorKeyframes[index].value,
+ animatorKeyframes[index + 1].value,
+ innerFraction
+ )
+ }
+}
+
+internal data class Keyframe<T>(
+ val fraction: Float,
+ val value: T,
+ val interpolator: Easing
+)
+
+internal enum class Ordering {
+ Together,
+ Sequentially
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal class StateVectorConfig : VectorConfig {
+
+ var rotationState: State<Float>? = null
+ var pivotXState: State<Float>? = null
+ var pivotYState: State<Float>? = null
+ var scaleXState: State<Float>? = null
+ var scaleYState: State<Float>? = null
+ var translateXState: State<Float>? = null
+ var translateYState: State<Float>? = null
+
+ // PathData is special because we have to animate its float fraction and interpolate the path.
+ var pathDataStatePair: Pair<State<Float>, PropertyValuesHolderPath>? = null
+ var fillColorState: State<Color>? = null
+ var strokeColorState: State<Color>? = null
+ var strokeWidthState: State<Float>? = null
+ var strokeAlphaState: State<Float>? = null
+ var fillAlphaState: State<Float>? = null
+ var trimPathStartState: State<Float>? = null
+ var trimPathEndState: State<Float>? = null
+ var trimPathOffsetState: State<Float>? = null
+
+ @Suppress("UNCHECKED_CAST")
+ override fun <T> getOrDefault(property: VectorProperty<T>, defaultValue: T): T {
+ return when (property) {
+ is VectorProperty.Rotation -> rotationState?.value ?: defaultValue
+ is VectorProperty.PivotX -> pivotXState?.value ?: defaultValue
+ is VectorProperty.PivotY -> pivotYState?.value ?: defaultValue
+ is VectorProperty.ScaleX -> scaleXState?.value ?: defaultValue
+ is VectorProperty.ScaleY -> scaleYState?.value ?: defaultValue
+ is VectorProperty.TranslateX -> translateXState?.value ?: defaultValue
+ is VectorProperty.TranslateY -> translateYState?.value ?: defaultValue
+ is VectorProperty.PathData ->
+ pathDataStatePair?.let { (state, holder) ->
+ holder.interpolate(state.value)
+ } ?: defaultValue
+ is VectorProperty.Fill -> fillColorState?.let { state ->
+ SolidColor(state.value)
+ } ?: defaultValue
+ is VectorProperty.FillAlpha -> fillAlphaState?.value ?: defaultValue
+ is VectorProperty.Stroke -> strokeColorState?.let { state ->
+ SolidColor(state.value)
+ } ?: defaultValue
+ is VectorProperty.StrokeLineWidth -> strokeWidthState?.value ?: defaultValue
+ is VectorProperty.StrokeAlpha -> strokeAlphaState?.value ?: defaultValue
+ is VectorProperty.TrimPathStart -> trimPathStartState?.value ?: defaultValue
+ is VectorProperty.TrimPathEnd -> trimPathEndState?.value ?: defaultValue
+ is VectorProperty.TrimPathOffset -> trimPathOffsetState?.value ?: defaultValue
+ } as T
+ }
+}
+
+private fun Easing.transpose(): Easing {
+ return Easing { x -> 1 - this.transform(1 - x) }
+}
+
+private fun lerp(start: List<PathNode>, stop: List<PathNode>, fraction: Float): List<PathNode> {
+ return start.zip(stop) { a, b -> lerp(a, b, fraction) }
+}
+
+/**
+ * Linearly interpolate between [start] and [stop] with [fraction] fraction between them.
+ */
+private fun lerp(start: PathNode, stop: PathNode, fraction: Float): PathNode {
+ return when (start) {
+ is PathNode.RelativeMoveTo -> {
+ require(stop is PathNode.RelativeMoveTo)
+ PathNode.RelativeMoveTo(
+ lerp(start.dx, stop.dx, fraction),
+ lerp(start.dy, stop.dy, fraction)
+ )
+ }
+ is PathNode.MoveTo -> {
+ require(stop is PathNode.MoveTo)
+ PathNode.MoveTo(
+ lerp(start.x, stop.x, fraction),
+ lerp(start.y, stop.y, fraction)
+ )
+ }
+ is PathNode.RelativeLineTo -> {
+ require(stop is PathNode.RelativeLineTo)
+ PathNode.RelativeLineTo(
+ lerp(start.dx, stop.dx, fraction),
+ lerp(start.dy, stop.dy, fraction)
+ )
+ }
+ is PathNode.LineTo -> {
+ require(stop is PathNode.LineTo)
+ PathNode.LineTo(
+ lerp(start.x, stop.x, fraction),
+ lerp(start.y, stop.y, fraction)
+ )
+ }
+ is PathNode.RelativeHorizontalTo -> {
+ require(stop is PathNode.RelativeHorizontalTo)
+ PathNode.RelativeHorizontalTo(
+ lerp(start.dx, stop.dx, fraction)
+ )
+ }
+ is PathNode.HorizontalTo -> {
+ require(stop is PathNode.HorizontalTo)
+ PathNode.HorizontalTo(
+ lerp(start.x, stop.x, fraction)
+ )
+ }
+ is PathNode.RelativeVerticalTo -> {
+ require(stop is PathNode.RelativeVerticalTo)
+ PathNode.RelativeVerticalTo(
+ lerp(start.dy, stop.dy, fraction)
+ )
+ }
+ is PathNode.VerticalTo -> {
+ require(stop is PathNode.VerticalTo)
+ PathNode.VerticalTo(
+ lerp(start.y, stop.y, fraction)
+ )
+ }
+ is PathNode.RelativeCurveTo -> {
+ require(stop is PathNode.RelativeCurveTo)
+ PathNode.RelativeCurveTo(
+ lerp(start.dx1, stop.dx1, fraction),
+ lerp(start.dy1, stop.dy1, fraction),
+ lerp(start.dx2, stop.dx2, fraction),
+ lerp(start.dy2, stop.dy2, fraction),
+ lerp(start.dx3, stop.dx3, fraction),
+ lerp(start.dy3, stop.dy3, fraction)
+ )
+ }
+ is PathNode.CurveTo -> {
+ require(stop is PathNode.CurveTo)
+ PathNode.CurveTo(
+ lerp(start.x1, stop.x1, fraction),
+ lerp(start.y1, stop.y1, fraction),
+ lerp(start.x2, stop.x2, fraction),
+ lerp(start.y2, stop.y2, fraction),
+ lerp(start.x3, stop.x3, fraction),
+ lerp(start.y3, stop.y3, fraction)
+ )
+ }
+ is PathNode.RelativeReflectiveCurveTo -> {
+ require(stop is PathNode.RelativeReflectiveCurveTo)
+ PathNode.RelativeReflectiveCurveTo(
+ lerp(start.dx1, stop.dx1, fraction),
+ lerp(start.dy1, stop.dy1, fraction),
+ lerp(start.dx2, stop.dx2, fraction),
+ lerp(start.dy2, stop.dy2, fraction)
+ )
+ }
+ is PathNode.ReflectiveCurveTo -> {
+ require(stop is PathNode.ReflectiveCurveTo)
+ PathNode.ReflectiveCurveTo(
+ lerp(start.x1, stop.x1, fraction),
+ lerp(start.y1, stop.y1, fraction),
+ lerp(start.x2, stop.x2, fraction),
+ lerp(start.y2, stop.y2, fraction)
+ )
+ }
+ is PathNode.RelativeQuadTo -> {
+ require(stop is PathNode.RelativeQuadTo)
+ PathNode.RelativeQuadTo(
+ lerp(start.dx1, stop.dx1, fraction),
+ lerp(start.dy1, stop.dy1, fraction),
+ lerp(start.dx2, stop.dx2, fraction),
+ lerp(start.dy2, stop.dy2, fraction)
+ )
+ }
+ is PathNode.QuadTo -> {
+ require(stop is PathNode.QuadTo)
+ PathNode.QuadTo(
+ lerp(start.x1, stop.x1, fraction),
+ lerp(start.y1, stop.y1, fraction),
+ lerp(start.x2, stop.x2, fraction),
+ lerp(start.y2, stop.y2, fraction)
+ )
+ }
+ is PathNode.RelativeReflectiveQuadTo -> {
+ require(stop is PathNode.RelativeReflectiveQuadTo)
+ PathNode.RelativeReflectiveQuadTo(
+ lerp(start.dx, stop.dx, fraction),
+ lerp(start.dy, stop.dy, fraction)
+ )
+ }
+ is PathNode.ReflectiveQuadTo -> {
+ require(stop is PathNode.ReflectiveQuadTo)
+ PathNode.ReflectiveQuadTo(
+ lerp(start.x, stop.x, fraction),
+ lerp(start.y, stop.y, fraction)
+ )
+ }
+ is PathNode.RelativeArcTo -> {
+ require(stop is PathNode.RelativeArcTo)
+ PathNode.RelativeArcTo(
+ lerp(start.horizontalEllipseRadius, stop.horizontalEllipseRadius, fraction),
+ lerp(start.verticalEllipseRadius, stop.verticalEllipseRadius, fraction),
+ lerp(start.theta, stop.theta, fraction),
+ start.isMoreThanHalf,
+ start.isPositiveArc,
+ lerp(start.arcStartDx, stop.arcStartDx, fraction),
+ lerp(start.arcStartDy, stop.arcStartDy, fraction)
+ )
+ }
+ is PathNode.ArcTo -> {
+ require(stop is PathNode.ArcTo)
+ PathNode.ArcTo(
+ lerp(start.horizontalEllipseRadius, stop.horizontalEllipseRadius, fraction),
+ lerp(start.verticalEllipseRadius, stop.verticalEllipseRadius, fraction),
+ lerp(start.theta, stop.theta, fraction),
+ start.isMoreThanHalf,
+ start.isPositiveArc,
+ lerp(start.arcStartX, stop.arcStartX, fraction),
+ lerp(start.arcStartY, stop.arcStartY, fraction)
+ )
+ }
+ PathNode.Close -> PathNode.Close
+ }
+}
diff --git a/compose/animation/animation-lint/src/test/java/androidx/compose/animation/lint/CrossfadeDetectorTest.kt b/compose/animation/animation-lint/src/test/java/androidx/compose/animation/lint/CrossfadeDetectorTest.kt
index ef3d713..c7cd1a4 100644
--- a/compose/animation/animation-lint/src/test/java/androidx/compose/animation/lint/CrossfadeDetectorTest.kt
+++ b/compose/animation/animation-lint/src/test/java/androidx/compose/animation/lint/CrossfadeDetectorTest.kt
@@ -23,6 +23,7 @@
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -80,6 +81,7 @@
"""
)
+ @Ignore // b/193270279
@Test
fun unreferencedParameters() {
lint().files(
@@ -132,6 +134,7 @@
)
}
+ @Ignore // b/193270279
@Test
fun unreferencedParameter_shadowedNames() {
lint().files(
@@ -243,6 +246,7 @@
CrossfadeStub,
Stubs.Composable
)
+ .allowCompilationErrors(true) // b/193270279
.run()
.expectClean()
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/build.gradle b/compose/animation/animation/integration-tests/animation-demos/build.gradle
index dd6ecf9..b03a158 100644
--- a/compose/animation/animation/integration-tests/animation-demos/build.gradle
+++ b/compose/animation/animation/integration-tests/animation-demos/build.gradle
@@ -16,6 +16,7 @@
implementation(project(":compose:ui:ui"))
implementation(project(":compose:ui:ui-text"))
implementation(project(":compose:animation:animation"))
+ implementation(project(":compose:animation:animation-graphics"))
implementation(project(":compose:animation:animation:animation-samples"))
implementation(project(":compose:animation:animation-core:animation-core-samples"))
implementation(project(":compose:foundation:foundation"))
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimatedVectorGraphicsDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimatedVectorGraphicsDemo.kt
new file mode 100644
index 0000000..854a5d7
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimatedVectorGraphicsDemo.kt
@@ -0,0 +1,194 @@
+/*
+ * 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.animation.demos
+
+import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.keyframes
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
+import androidx.compose.animation.graphics.res.animatedVectorResource
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.graphics.vector.Group
+import androidx.compose.ui.graphics.vector.Path
+import androidx.compose.ui.graphics.vector.PathData
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.unit.dp
+
+@OptIn(ExperimentalAnimationGraphicsApi::class)
+@Composable
+fun AnimatedVectorGraphicsDemo() {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.SpaceEvenly,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ val image = animatedVectorResource(R.drawable.ic_hourglass_animated)
+ var atEnd by remember { mutableStateOf(false) }
+ Image(
+ painter = image.painterFor(atEnd),
+ contentDescription = "AnimatedImageVector",
+ modifier = Modifier.size(200.dp).clickable {
+ atEnd = !atEnd
+ },
+ contentScale = ContentScale.Crop
+ )
+
+ var toggle by remember { mutableStateOf(false) }
+ Image(
+ painter = createSampleVectorPainter(toggle),
+ contentDescription = "Transition with vector graphics",
+ modifier = Modifier.size(200.dp).clickable {
+ toggle = !toggle
+ },
+ contentScale = ContentScale.Crop
+ )
+ }
+}
+
+@Composable
+fun createSampleVectorPainter(toggle: Boolean): Painter {
+ return rememberVectorPainter(
+ defaultWidth = 24.dp,
+ defaultHeight = 24.dp,
+ viewportWidth = 24f,
+ viewportHeight = 24f,
+ name = "sample"
+ ) { _, _ ->
+ val transition = updateTransition(targetState = toggle, label = "sample")
+ val duration = 3000
+ Path(
+ pathData = PathData {
+ horizontalLineTo(24f)
+ verticalLineTo(24f)
+ horizontalLineTo(0f)
+ close()
+ },
+ fill = SolidColor(Color.Cyan)
+ )
+ val rotation by transition.animateFloat(
+ transitionSpec = {
+ if (targetState) {
+ keyframes {
+ durationMillis = duration
+ 0f at 0
+ 360f at duration with LinearEasing
+ }
+ } else {
+ spring(
+ dampingRatio = Spring.DampingRatioMediumBouncy,
+ stiffness = Spring.StiffnessVeryLow
+ )
+ }
+ },
+ label = "rotation"
+ ) { state ->
+ if (state) 360f else 0f
+ }
+
+ @Suppress("UnusedTransitionTargetStateParameter")
+ val translationX by transition.animateFloat(
+ transitionSpec = {
+ if (targetState) {
+ keyframes {
+ durationMillis = duration
+ -6f at 500
+ 6f at 1500
+ -6f at 2000
+ 6f at 2500
+ }
+ } else {
+ spring(
+ dampingRatio = Spring.DampingRatioHighBouncy,
+ stiffness = Spring.StiffnessLow
+ )
+ }
+ },
+ label = "translationX"
+ ) { 0f }
+
+ @Suppress("UnusedTransitionTargetStateParameter")
+ val translationY by transition.animateFloat(
+ transitionSpec = {
+ if (targetState) {
+ keyframes {
+ durationMillis = duration
+ -6f at 1000
+ 6f at 2000
+ }
+ } else {
+ spring()
+ }
+ },
+ label = "translationY"
+ ) { 0f }
+ Group(
+ name = "rectangle",
+ rotation = rotation,
+ translationX = translationX,
+ translationY = translationY,
+ pivotX = 12f,
+ pivotY = 12f
+ ) {
+ val fillColor by transition.animateColor(
+ transitionSpec = {
+ if (targetState) {
+ keyframes {
+ durationMillis = duration
+ Color.Red at 0
+ Color.Blue at duration with LinearEasing
+ }
+ } else {
+ spring()
+ }
+ },
+ label = "fillColor"
+ ) { state ->
+ if (state) Color.Blue else Color.Red
+ }
+ Path(
+ pathData = PathData {
+ moveTo(8f, 8f)
+ lineTo(16f, 8f)
+ lineTo(16f, 16f)
+ lineTo(8f, 16f)
+ close()
+ },
+ fill = SolidColor(fillColor)
+ )
+ }
+ }
+}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
index 9d20a99..1263471 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
@@ -59,6 +59,12 @@
ComposableDemo("Swipe to dismiss") { SwipeToDismissDemo() },
)
),
+ DemoCategory(
+ "Graphics Animation Demos",
+ listOf(
+ ComposableDemo("Animated Vector Graphics") { AnimatedVectorGraphicsDemo() },
+ )
+ ),
DemoCategory(
"â›” DO NOT ENTER â›”",
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/ic_hourglass_animated.xml b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/ic_hourglass_animated.xml
new file mode 100644
index 0000000..bfc72af
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/res/drawable/ic_hourglass_animated.xml
@@ -0,0 +1,156 @@
+<?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.
+-->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <target android:name="hourglass_frame">
+ <aapt:attr name="android:animation">
+ <objectAnimator
+ android:duration="333"
+ android:interpolator="@android:interpolator/accelerate_decelerate"
+ android:propertyName="rotation"
+ android:valueFrom="0"
+ android:valueTo="180" />
+ </aapt:attr>
+ </target>
+ <target android:name="fill_outlines">
+ <aapt:attr name="android:animation">
+ <objectAnimator
+ android:duration="333"
+ android:interpolator="@android:interpolator/accelerate_decelerate"
+ android:propertyName="rotation"
+ android:valueFrom="0"
+ android:valueTo="180" />
+ </aapt:attr>
+ </target>
+ <target android:name="mask_sand">
+ <aapt:attr name="android:animation">
+ <objectAnimator
+ android:duration="1000"
+ android:interpolator="@android:interpolator/accelerate_decelerate"
+ android:propertyName="pathData"
+ android:startOffset="333"
+ android:valueFrom="M 24 13.3999938965 c 0 0.0 -24 0.0 -24 0.0
+ c 0 0.0 0 10.6000061035 0 10.6000061035 c 0 0 24 0 24 0
+ c 0 0 0 -10.6000061035 0 -10.6000061035 Z"
+ android:valueTo="M 24 0.00173950195312 c 0 0.0 -24 0.0 -24 0.0
+ c 0 0.0 0 10.6982574463 0 10.6982574463 c 0 0.0 24 0.0 24 0.0
+ c 0 0.0 0 -10.6982574463 0 -10.6982574463 Z"
+ android:valueType="pathType" />
+ </aapt:attr>
+ </target>
+ <aapt:attr name="android:drawable">
+ <vector
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <group
+ android:name="hourglass_frame"
+ android:scaleX="0.75"
+ android:scaleY="0.75"
+ android:translateX="12"
+ android:translateY="12">
+ <group
+ android:name="hourglass_frame_pivot"
+ android:translateX="-12"
+ android:translateY="-12">
+ <group
+ android:name="group_bottom"
+ android:translateX="12"
+ android:translateY="6.5">
+ <path
+ android:name="path_bottom"
+ android:fillColor="#FF777777"
+ android:pathData="M 6.52099609375 -3.89300537109
+ c 0.0 0.0 -6.52099609375 6.87901306152 -6.52099609375 6.87901306152
+ c 0 0.0 -6.52099609375 -6.87901306152 -6.52099609375 -6.87901306152
+ c 0.0 0.0 13.0419921875 0.0 13.0419921875 0.0 Z M 9.99800109863 -6.5
+ c 0.0 0.0 -19.9960021973 0.0 -19.9960021973 0.0
+ c -0.890991210938 0.0 -1.33700561523 1.07699584961 -0.707000732422
+ 1.70700073242
+ c 0.0 0.0 10.7050018311 11.2929992676 10.7050018311 11.2929992676
+ c 0 0.0 10.7050018311 -11.2929992676 10.7050018311 -11.2929992676
+ c 0.630004882812 -0.630004882812 0.183990478516 -1.70700073242
+ -0.707000732422 -1.70700073242 Z" />
+ </group>
+ <group
+ android:name="group_top"
+ android:translateX="12"
+ android:translateY="17.5">
+ <path
+ android:name="path_top"
+ android:fillColor="#FF777777"
+ android:pathData="M 0 -2.98600769043
+ c 0 0.0 6.52099609375 6.87901306152 6.52099609375 6.87901306152
+ c 0.0 0.0 -13.0419921875 0.0 -13.0419921875 0.0
+ c 0.0 0.0 6.52099609375 -6.87901306152 6.52099609375 -6.87901306152
+ Z M 0 -6.5
+ c 0 0.0 -10.7050018311 11.2929992676 -10.7050018311 11.2929992676
+ c -0.630004882812 0.630004882812 -0.184005737305 1.70700073242
+ 0.707000732422 1.70700073242
+ c 0.0 0.0 19.9960021973 0.0 19.9960021973 0.0
+ c 0.890991210938 0.0 1.33699035645 -1.07699584961 0.707000732422
+ -1.70700073242
+ c 0.0 0.0 -10.7050018311 -11.2929992676 -10.7050018311
+ -11.2929992676 Z" />
+ </group>
+ </group>
+ </group>
+ <group
+ android:name="fill_outlines"
+ android:scaleX="0.75"
+ android:scaleY="0.75"
+ android:translateX="12"
+ android:translateY="12">
+ <group
+ android:name="fill_outlines_pivot"
+ android:translateX="-12"
+ android:translateY="-12">
+ <clip-path
+ android:name="mask_sand"
+ android:pathData="M 24 13.3999938965 c 0 0.0 -24 0.0 -24 0.0
+ c 0 0.0 0 10.6000061035 0 10.6000061035 c 0 0 24 0 24 0
+ c 0 0 0 -10.6000061035 0 -10.6000061035 Z" />
+ <group
+ android:name="group_sand"
+ android:translateX="12"
+ android:translateY="12">
+ <path
+ android:name="path_sand"
+ android:fillColor="#FF777777"
+ android:pathData="M 10.7100067139 10.2900085449
+ c 0.629989624023 0.629989624023 0.179992675781 1.70999145508
+ -0.710006713867 1.70999145508
+ c 0 0 -20 0 -20 0
+ c -0.889999389648 0 -1.33999633789 -1.08000183105 -0.710006713867
+ -1.70999145508
+ c 0.0 0.0 9.76000976562 -10.2900085449 9.76000976563 -10.2900085449
+ c 0.0 0 -9.76000976562 -10.2899932861 -9.76000976563 -10.2899932861
+ c -0.629989624023 -0.630004882812 -0.179992675781 -1.71000671387
+ 0.710006713867 -1.71000671387
+ c 0 0 20 0 20 0
+ c 0.889999389648 0 1.33999633789 1.08000183105 0.710006713867
+ 1.71000671387
+ c 0.0 0.0 -9.76000976562 10.2899932861 -9.76000976563 10.2899932861
+ c 0.0 0 9.76000976562 10.2900085449 9.76000976563 10.2900085449
+ Z" />
+ </group>
+ </group>
+ </group>
+ </vector>
+ </aapt:attr>
+</animated-vector>
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt
index 8748d3c..13030b1 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt
@@ -1530,6 +1530,19 @@
)
@Test
+ fun testComposableMap(): Unit = codegen(
+ """
+ class Repro {
+ private val composables = linkedMapOf<String, @Composable () -> Unit>()
+
+ fun doSomething() {
+ composables[""]
+ }
+ }
+ """
+ )
+
+ @Test
fun testComposableColorFunInterfaceExample(): Unit = checkApi(
"""
import androidx.compose.material.Text
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTypeRemapper.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTypeRemapper.kt
index b465f4a..0109a61 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTypeRemapper.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTypeRemapper.kt
@@ -62,12 +62,12 @@
import org.jetbrains.kotlin.ir.util.SymbolRenamer
import org.jetbrains.kotlin.ir.util.TypeRemapper
import org.jetbrains.kotlin.ir.util.TypeTranslator
+import org.jetbrains.kotlin.ir.util.fqNameForIrSerialization
import org.jetbrains.kotlin.ir.util.functions
import org.jetbrains.kotlin.ir.util.hasAnnotation
import org.jetbrains.kotlin.ir.util.patchDeclarationParents
import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
import org.jetbrains.kotlin.name.FqName
-import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.Variance
@@ -462,8 +462,5 @@
)
}
-@OptIn(ObsoleteDescriptorBasedAPI::class)
private fun IrConstructorCall.isComposableAnnotation() =
- @Suppress("DEPRECATION")
- this.symbol.descriptor.returnType.constructor.declarationDescriptor?.fqNameSafe ==
- ComposeFqNames.Composable
+ this.symbol.owner.parent.fqNameForIrSerialization == ComposeFqNames.Composable
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/api/current.txt b/compose/foundation/foundation/api/current.txt
index 1d1d50f..30b2b4e 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -565,6 +565,9 @@
package androidx.compose.foundation.text {
+ public final class AndroidCursorHandle_androidKt {
+ }
+
public final class BasicTextFieldKt {
method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 59fae91..4897c07 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -600,6 +600,9 @@
package androidx.compose.foundation.text {
+ public final class AndroidCursorHandle_androidKt {
+ }
+
public final class BasicTextFieldKt {
method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 1d1d50f..30b2b4e 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -565,6 +565,9 @@
package androidx.compose.foundation.text {
+ public final class AndroidCursorHandle_androidKt {
+ }
+
public final class BasicTextFieldKt {
method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
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/androidMain/kotlin/androidx/compose/foundation/text/AndroidCursorHandle.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/AndroidCursorHandle.android.kt
new file mode 100644
index 0000000..4b5d938
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/AndroidCursorHandle.android.kt
@@ -0,0 +1,174 @@
+/*
+ * 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.foundation.text
+
+import androidx.compose.foundation.text.selection.LocalTextSelectionColors
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.PopupPositionProvider
+import androidx.compose.ui.window.PopupProperties
+import kotlin.math.roundToInt
+
+private const val Sqrt2 = 1.41421356f
+internal val CursorHandleHeight = 25.dp
+internal val CursorHandleWidth = CursorHandleHeight * 2f / (1 + Sqrt2)
+
+@Composable
+internal actual fun CursorHandle(
+ handlePosition: Offset,
+ modifier: Modifier,
+ content: @Composable (() -> Unit)?
+) {
+ CursorHandlePopup(
+ handlePosition = handlePosition
+ ) {
+ if (content == null) {
+ DefaultCursorHandle(
+ modifier = modifier,
+ )
+ } else content()
+ }
+}
+
+@Composable
+/*@VisibleForTesting*/
+internal fun DefaultCursorHandle(
+ modifier: Modifier,
+) {
+ val handleColor = LocalTextSelectionColors.current.handleColor
+ HandleDrawLayout(
+ modifier = modifier,
+ width = CursorHandleWidth,
+ height = CursorHandleHeight
+ ) {
+ drawPath(CursorHandleCache.createPath(this), handleColor)
+ }
+}
+
+/**
+ * Class used to cache a Path object to represent a selection handle
+ * based on the given handle direction
+ */
+private class CursorHandleCache {
+ companion object {
+ private var cachedPath: Path? = null
+
+ fun createPath(density: Density): Path {
+ // Use the cached path if we've already created one before.
+ val path = cachedPath ?: Path().apply {
+ // This is the first time we create the Path.
+ with(density) {
+ val height = CursorHandleHeight.toPx()
+ val radius = height / (1 + Sqrt2)
+ val edge = radius * Sqrt2 / 2
+ moveTo(x = radius - edge, y = height - radius - edge)
+ lineTo(x = radius, y = 0f)
+ lineTo(x = radius + edge, y = height - radius - edge)
+ arcTo(
+ rect = Rect(
+ top = height - 2f * radius,
+ left = 0f,
+ bottom = height,
+ right = 2 * radius
+ ),
+ startAngleDegrees = -45f,
+ sweepAngleDegrees = 270f,
+ forceMoveTo = true
+ )
+ }
+ }
+ cachedPath = path
+ return path
+ }
+ }
+}
+
+/**
+ * Simple container to perform drawing of selection handles. This layout takes size on the screen
+ * according to [width] and [height] params and performs drawing in this space as specified in
+ * [onCanvas]
+ */
+@Composable
+private fun HandleDrawLayout(
+ modifier: Modifier,
+ width: Dp,
+ height: Dp,
+ onCanvas: DrawScope.() -> Unit
+) {
+ Layout({}, modifier.drawBehind(onCanvas)) { _, _ ->
+ // take width and height space of the screen and allow draw modifier to draw inside of it
+ layout(width.roundToPx(), height.roundToPx()) {
+ // this layout has no children, only draw modifier.
+ }
+ }
+}
+
+@Composable
+private fun CursorHandlePopup(
+ handlePosition: Offset,
+ content: @Composable () -> Unit
+) {
+ val intOffset = IntOffset(handlePosition.x.roundToInt(), handlePosition.y.roundToInt())
+
+ val popupPositioner = remember(intOffset) {
+ CursorHandlePositionProvider(intOffset)
+ }
+
+ Popup(
+ popupPositionProvider = popupPositioner,
+ properties = PopupProperties(
+ excludeFromSystemGesture = true,
+ clippingEnabled = false
+ ),
+ content = content
+ )
+}
+
+/**
+ * This [PopupPositionProvider] for [CursorHandlePopup]. It will position the cursor handle
+ * to the [offset] in its anchor layout. For the cursor handle, the middle of the top will be
+ * positioned to [offset].
+ */
+/*@VisibleForTesting*/
+internal class CursorHandlePositionProvider(val offset: IntOffset) : PopupPositionProvider {
+ override fun calculatePosition(
+ anchorBounds: IntRect,
+ windowSize: IntSize,
+ layoutDirection: LayoutDirection,
+ popupContentSize: IntSize
+ ): IntOffset {
+ return IntOffset(
+ x = anchorBounds.left + offset.x - popupContentSize.width / 2,
+ y = anchorBounds.top + offset.y
+ )
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
index 7771b2c..a577d7c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
@@ -16,6 +16,14 @@
package androidx.compose.foundation.gestures
+// Note, that there is a copy-paste version of this file (DragGestureDetectorCopy.kt), don't
+// forget to change it too.
+//
+// We can't make *PointerSlop* functions public just yet because the new pointer API isn't ready.
+
+// TODO(b/193549931): when the new pointer API will be ready we should make *PointerSlop*
+// functions public
+
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerEvent
@@ -23,6 +31,7 @@
import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.input.pointer.changedToUp
import androidx.compose.ui.input.pointer.positionChangeConsumed
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
@@ -33,6 +42,7 @@
import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed
import androidx.compose.ui.input.pointer.positionChangedIgnoreConsumed
import androidx.compose.ui.platform.ViewConfiguration
+import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastFirstOrNull
@@ -69,11 +79,26 @@
pointerId: PointerId,
onTouchSlopReached: (change: PointerInputChange, overSlop: Offset) -> Unit
): PointerInputChange? {
+ return awaitPointerSlopOrCancellation(
+ pointerId,
+ PointerType.Touch,
+ onTouchSlopReached
+ )
+}
+
+// TODO(demin): probably we can get rid of copy-paste and reuse awaitPointerSlopOrCancellation
+// at the bottom of this file. We just need to change its interface:
+// awaitPointerSlopOrCancellation(getDragValue: (Offset) -> Offset)
+internal suspend fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
+ pointerId: PointerId,
+ pointerType: PointerType,
+ onPointerSlopReached: (change: PointerInputChange, overSlop: Offset) -> Unit
+): PointerInputChange? {
if (currentEvent.isPointerUp(pointerId)) {
return null // The pointer has already been lifted, so the gesture is canceled
}
var offset = Offset.Zero
- val touchSlop = viewConfiguration.touchSlop
+ val touchSlop = viewConfiguration.pointerSlop(pointerType)
var pointer = pointerId
@@ -96,7 +121,7 @@
var acceptedDrag = false
if (distance >= touchSlop) {
val touchSlopOffset = offset / distance * touchSlop
- onTouchSlopReached(dragEvent, offset - touchSlopOffset)
+ onPointerSlopReached(dragEvent, offset - touchSlopOffset)
if (dragEvent.positionChangeConsumed()) {
acceptedDrag = true
} else {
@@ -204,7 +229,10 @@
var drag: PointerInputChange?
var overSlop = Offset.Zero
do {
- drag = awaitTouchSlopOrCancellation(down.id) { change, over ->
+ drag = awaitPointerSlopOrCancellation(
+ down.id,
+ down.type
+ ) { change, over ->
change.consumePositionChange()
overSlop = over
}
@@ -308,9 +336,21 @@
suspend fun AwaitPointerEventScope.awaitVerticalTouchSlopOrCancellation(
pointerId: PointerId,
onTouchSlopReached: (change: PointerInputChange, overSlop: Float) -> Unit
-) = awaitTouchSlopOrCancellation(
+) = awaitPointerSlopOrCancellation(
pointerId = pointerId,
- onTouchSlopReached = onTouchSlopReached,
+ pointerType = PointerType.Touch,
+ onPointerSlopReached = onTouchSlopReached,
+ getDragDirectionValue = { it.y }
+)
+
+internal suspend fun AwaitPointerEventScope.awaitVerticalPointerSlopOrCancellation(
+ pointerId: PointerId,
+ pointerType: PointerType,
+ onTouchSlopReached: (change: PointerInputChange, overSlop: Float) -> Unit
+) = awaitPointerSlopOrCancellation(
+ pointerId = pointerId,
+ pointerType = pointerType,
+ onPointerSlopReached = onTouchSlopReached,
getDragDirectionValue = { it.y }
)
@@ -396,7 +436,7 @@
awaitPointerEventScope {
val down = awaitFirstDown(requireUnconsumed = false)
var overSlop = 0f
- val drag = awaitVerticalTouchSlopOrCancellation(down.id) { change, over ->
+ val drag = awaitVerticalPointerSlopOrCancellation(down.id, down.type) { change, over ->
change.consumePositionChange()
overSlop = over
}
@@ -443,9 +483,21 @@
suspend fun AwaitPointerEventScope.awaitHorizontalTouchSlopOrCancellation(
pointerId: PointerId,
onTouchSlopReached: (change: PointerInputChange, overSlop: Float) -> Unit
-) = awaitTouchSlopOrCancellation(
+) = awaitPointerSlopOrCancellation(
pointerId = pointerId,
- onTouchSlopReached = onTouchSlopReached,
+ pointerType = PointerType.Touch,
+ onPointerSlopReached = onTouchSlopReached,
+ getDragDirectionValue = { it.x }
+)
+
+internal suspend fun AwaitPointerEventScope.awaitHorizontalPointerSlopOrCancellation(
+ pointerId: PointerId,
+ pointerType: PointerType,
+ onPointerSlopReached: (change: PointerInputChange, overSlop: Float) -> Unit
+) = awaitPointerSlopOrCancellation(
+ pointerId = pointerId,
+ pointerType = pointerType,
+ onPointerSlopReached = onPointerSlopReached,
getDragDirectionValue = { it.x }
)
@@ -528,7 +580,10 @@
awaitPointerEventScope {
val down = awaitFirstDown(requireUnconsumed = false)
var overSlop = 0f
- val drag = awaitHorizontalTouchSlopOrCancellation(down.id) { change, over ->
+ val drag = awaitHorizontalPointerSlopOrCancellation(
+ down.id,
+ down.type
+ ) { change, over ->
change.consumePositionChange()
overSlop = over
}
@@ -616,34 +671,35 @@
}
/**
- * Waits for drag motion along one axis based on [getDragDirectionValue] to pass touch slop,
+ * Waits for drag motion along one axis based on [getDragDirectionValue] to pass pointer slop,
* using [pointerId] as the pointer to examine. If [pointerId] is raised, another pointer
* from those that are down will be chosen to lead the gesture, and if none are down,
- * `null` is returned. If [pointerId] is not down when [awaitTouchSlopOrCancellation] is called,
+ * `null` is returned. If [pointerId] is not down when [awaitPointerSlopOrCancellation] is called,
* then `null` is returned.
*
- * When touch slop is detected, [onTouchSlopReached] is called with the change and the distance
- * beyond the touch slop. [getDragDirectionValue] should return the position change in the direction
- * of the drag axis. If [onTouchSlopReached] does not consume the position change, touch slop
- * will not have been considered detected and the detection will continue or, if it is consumed,
- * the [PointerInputChange] that was consumed will be returned.
+ * When pointer slop is detected, [onPointerSlopReached] is called with the change and the distance
+ * beyond the pointer slop. [getDragDirectionValue] should return the position change in the
+ * direction of the drag axis. If [onPointerSlopReached] does not consume the position change,
+ * pointer slop will not have been considered detected and the detection will continue or,
+ * if it is consumed, the [PointerInputChange] that was consumed will be returned.
*
* This works with [awaitTouchSlopOrCancellation] for the other axis to ensure that only horizontal
* or vertical dragging is done, but not both.
*
- * @return The [PointerInputChange] of the event that was consumed in [onTouchSlopReached] or
+ * @return The [PointerInputChange] of the event that was consumed in [onPointerSlopReached] or
* `null` if all pointers are raised or the position change was consumed by another gesture
* detector.
*/
-private suspend inline fun AwaitPointerEventScope.awaitTouchSlopOrCancellation(
+private suspend inline fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
pointerId: PointerId,
- onTouchSlopReached: (PointerInputChange, Float) -> Unit,
+ pointerType: PointerType,
+ onPointerSlopReached: (PointerInputChange, Float) -> Unit,
getDragDirectionValue: (Offset) -> Float
): PointerInputChange? {
if (currentEvent.isPointerUp(pointerId)) {
return null // The pointer has already been lifted, so the gesture is canceled
}
- val touchSlop = viewConfiguration.touchSlop
+ val touchSlop = viewConfiguration.pointerSlop(pointerType)
var pointer: PointerId = pointerId
var totalPositionChange = 0f
@@ -675,7 +731,7 @@
return null
}
} else {
- onTouchSlopReached(
+ onPointerSlopReached(
dragEvent,
totalPositionChange - (sign(totalPositionChange) * touchSlop)
)
@@ -742,4 +798,23 @@
}
private fun PointerEvent.isPointerUp(pointerId: PointerId): Boolean =
- changes.fastFirstOrNull { it.id == pointerId }?.pressed != true
\ No newline at end of file
+ changes.fastFirstOrNull { it.id == pointerId }?.pressed != true
+
+// This value was determined using experiments and common sense.
+// We can't use zero slop, because some hypothetical desktop/mobile devices can send
+// pointer events with a very high precision (but I haven't encountered any that send
+// events with less than 1px precision)
+private val mouseSlop = 0.125.dp
+private val defaultTouchSlop = 18.dp // The default touch slop on Android devices
+private val mouseToTouchSlopRatio = mouseSlop / defaultTouchSlop
+
+// TODO(demin): consider this as part of ViewConfiguration class after we make *PointerSlop*
+// functions public (see the comment at the top of the file).
+// After it will be a public API, we should get rid of `touchSlop / 144` and return absolute
+// value 0.125.dp.toPx(). It is not possible right now, because we can't access density.
+internal fun ViewConfiguration.pointerSlop(pointerType: PointerType): Float {
+ return when (pointerType) {
+ PointerType.Mouse -> touchSlop * mouseToTouchSlopRatio
+ else -> touchSlop
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
index a2b56c6..61f4842 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
@@ -301,14 +301,14 @@
down to 0f
} else {
var initialDelta = 0f
- val postTouchSlop = { event: PointerInputChange, offset: Float ->
+ val postPointerSlop = { event: PointerInputChange, offset: Float ->
event.consumePositionChange()
initialDelta = offset
}
val afterSlopResult = if (orientation == Orientation.Vertical) {
- awaitVerticalTouchSlopOrCancellation(down.id, postTouchSlop)
+ awaitVerticalPointerSlopOrCancellation(down.id, down.type, postPointerSlop)
} else {
- awaitHorizontalTouchSlopOrCancellation(down.id, postTouchSlop)
+ awaitHorizontalPointerSlopOrCancellation(down.id, down.type, postPointerSlop)
}
if (afterSlopResult != null) afterSlopResult to initialDelta else null
}
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/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index fde83fb..7a45e2d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -37,11 +37,13 @@
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.FirstBaseline
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
@@ -286,7 +288,7 @@
Modifier.tapPressTextFieldModifier(interactionSource, enabled) { offset ->
tapToFocus(state, focusRequester, !readOnly)
if (state.hasFocus) {
- if (!state.selectionIsOn) {
+ if (state.handleState != HandleState.Selection) {
state.layoutResult?.let { layoutResult ->
TextFieldDelegate.setCursorOffset(
offset,
@@ -295,6 +297,10 @@
offsetMapping,
onValueChangeWrapper
)
+ // Won't enter cursor state when text is empty.
+ if (state.textDelegate.text.isNotEmpty()) {
+ state.handleState = HandleState.Cursor
+ }
}
} else {
manager.deselect(offset)
@@ -325,7 +331,7 @@
val onPositionedModifier = Modifier.onGloballyPositioned {
if (textInputService != null) {
state.layoutCoordinates = it
- if (state.selectionIsOn) {
+ if (state.handleState == HandleState.Selection) {
if (state.showFloatingToolbar) {
manager.showSelectionToolbar()
} else {
@@ -442,7 +448,13 @@
value = value,
editProcessor = state.processor,
imeOptions = imeOptions,
- onValueChange = onValueChangeWrapper,
+ onValueChange = {
+ // Text has been modified, enter the Edit state.
+ if (state.textDelegate.text.text != it.annotatedString.text) {
+ state.handleState = HandleState.None
+ }
+ onValueChangeWrapper(it)
+ },
onImeActionPerformed = onImeActionPerformedWrapper
)
}
@@ -530,17 +542,52 @@
SelectionToolbarAndHandles(
manager = manager,
show = enabled &&
+ state.handleState == HandleState.Selection &&
state.hasFocus &&
- state.selectionIsOn &&
state.layoutCoordinates != null &&
state.layoutCoordinates!!.isAttached &&
isInTouchMode
)
+ if (state.handleState == HandleState.Cursor) {
+ TextFieldCursorHandle(manager = manager)
+ }
}
}
}
}
+/**
+ * The selection handle state of the TextField. It can be None, Selection or Cursor.
+ * It determines whether the selection handle, cursor handle or only cursor is shown. And how
+ * TextField handles gestures.
+ */
+internal enum class HandleState {
+ /**
+ * No selection is active in this TextField. This is the initial state of the TextField.
+ * If the user long click on the text and start selection, the TextField will exit this state
+ * and enters [HandleState.Selection] state. If the user tap on the text, the TextField
+ * will exit this state and enters [HandleState.Cursor] state.
+ */
+ None,
+
+ /**
+ * Selection handle is displayed for this TextField. User can drag the selection handle to
+ * change the selected text. If the user start editing the text, the TextField will exit this
+ * state and enters [HandleState.None] state. If the user tap on the text, the TextField
+ * will exit this state and enters [HandleState.Cursor] state.
+ */
+ Selection,
+
+ /**
+ * Cursor handle is displayed for this TextField. User can drag the cursor handle to change
+ * the cursor position. If the user start editing the text, the TextField will exit this
+ * state and enters [HandleState.None] state. If the user long click on the text and start
+ * selection, the TextField will exit this state and enters [HandleState.Selection] state.
+ * Also notice that TextField won't enter this state if the current input text is empty.
+ */
+ Cursor
+}
+
@OptIn(InternalFoundationTextApi::class)
internal class TextFieldState(
var textDelegate: TextDelegate
@@ -571,16 +618,23 @@
var layoutResult: TextLayoutResultProxy? = null
/**
- * The gesture detector status, to indicate whether current status is selection or editing.
+ * The gesture detector state, to indicate whether current state is selection, cursor
+ * or editing.
*
- * In the editing mode, there is no selection shown, only cursor is shown. To enter the editing
- * mode from selection mode, just tap on the screen.
+ * In the none state, no selection or cursor handle is shown, only the cursor is shown.
+ * TextField is initially in this state. To enter this state, input anything from the
+ * keyboard and modify the text.
*
- * In the selection mode, there is no cursor shown, only selection is shown. To enter
+ * In the selection state, there is no cursor shown, only selection is shown. To enter
* the selection mode, just long press on the screen. In this mode, finger movement on the
* screen changes selection instead of moving the cursor.
+ *
+ * In the cursor state, no selection is shown, and the cursor and the cursor handle are shown.
+ * To enter the cursor state, tap anywhere within the TextField.(The TextField will stay in the
+ * edit state if the current text is empty.) In this mode, finger movement on the screen
+ * moves the cursor.
*/
- var selectionIsOn by mutableStateOf(false)
+ var handleState by mutableStateOf(HandleState.None)
/**
* A flag to check if the selection start or end handle is being dragged.
@@ -658,6 +712,7 @@
}
}
+@OptIn(InternalFoundationTextApi::class)
private fun notifyTextInputServiceOnFocusChange(
textInputService: TextInputService,
state: TextFieldState,
@@ -673,7 +728,13 @@
value,
state.processor,
imeOptions,
- onValueChange,
+ {
+ onValueChange(it)
+ if (state.textDelegate.text.text != it.annotatedString.text) {
+ // Text has been changed, set the selection mode to Edit.
+ state.handleState = HandleState.None
+ }
+ },
onImeActionPerformed
).also { newSession ->
state.layoutCoordinates?.let { coords ->
@@ -739,3 +800,31 @@
}
} else manager.hideSelectionToolbar()
}
+
+@Composable
+internal fun TextFieldCursorHandle(manager: TextFieldSelectionManager) {
+ val offset = manager.offsetMapping.originalToTransformed(manager.value.selection.start)
+ val observer = remember(manager) { manager.cursorDragObserver() }
+ manager.state?.layoutResult?.value?.let {
+ val cursorRect = it.getCursorRect(
+ offset.coerceIn(0, it.layoutInput.text.length)
+ )
+ val x = with(LocalDensity.current) {
+ cursorRect.left + DefaultCursorThickness.toPx() / 2
+ }
+ CursorHandle(
+ handlePosition = Offset(x, cursorRect.bottom),
+ modifier = Modifier.pointerInput(observer) {
+ detectDragGesturesWithObserver(observer)
+ },
+ content = null
+ )
+ }
+}
+
+@Composable
+internal expect fun CursorHandle(
+ handlePosition: Offset,
+ modifier: Modifier,
+ content: @Composable (() -> Unit)?
+)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
index 9d8b6f2..c4fc399 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
@@ -17,6 +17,7 @@
package androidx.compose.foundation.text.selection
import androidx.compose.foundation.text.InternalFoundationTextApi
+import androidx.compose.foundation.text.HandleState
import androidx.compose.foundation.text.TextDragObserver
import androidx.compose.foundation.text.TextFieldState
import androidx.compose.foundation.text.UndoManager
@@ -354,6 +355,50 @@
}
/**
+ * [TextDragObserver] for dragging the cursor to change the selection in TextField.
+ */
+ internal fun cursorDragObserver(): TextDragObserver {
+ return object : TextDragObserver {
+ override fun onStart(startPoint: Offset) {
+ // The position of the character where the drag gesture should begin. This is in
+ // the composable coordinates.
+ dragBeginPosition = getAdjustedCoordinates(getHandlePosition(true))
+ // Zero out the total distance that being dragged.
+ dragTotalDistance = Offset.Zero
+ state?.draggingHandle = true
+ }
+
+ override fun onDrag(delta: Offset) {
+ dragTotalDistance += delta
+
+ state?.layoutResult?.value?.let { layoutResult ->
+ val offset =
+ layoutResult.getOffsetForPosition(dragBeginPosition + dragTotalDistance)
+
+ val newSelection = TextRange(offset, offset)
+
+ // Nothing changed, skip onValueChange hand hapticFeedback.
+ if (newSelection == value.selection) return
+
+ hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
+ onValueChange(
+ createTextFieldValue(
+ annotatedString = value.annotatedString,
+ selection = newSelection
+ )
+ )
+ }
+ }
+
+ override fun onStop() {
+ state?.draggingHandle = false
+ }
+
+ override fun onCancel() {}
+ }
+ }
+
+ /**
* The method to record the required state values on entering the selection mode.
*
* Is triggered on long press or accessibility action.
@@ -364,7 +409,7 @@
}
oldValue = value
state?.showFloatingToolbar = true
- setSelectionStatus(true)
+ setHandleState(HandleState.Selection)
}
/**
@@ -374,7 +419,7 @@
*/
internal fun exitSelectionMode() {
state?.showFloatingToolbar = false
- setSelectionStatus(false)
+ setHandleState(HandleState.None)
}
internal fun deselect(position: Offset? = null) {
@@ -392,7 +437,15 @@
val newValue = value.copy(selection = TextRange(newCursorOffset))
onValueChange(newValue)
}
- setSelectionStatus(false)
+
+ // If a new cursor position is given and the text is not empty, enter the
+ // HandleState.Cursor state.
+ val selectionMode = if (position != null && value.text.isNotEmpty()) {
+ HandleState.Cursor
+ } else {
+ HandleState.None
+ }
+ setHandleState(selectionMode)
hideSelectionToolbar()
}
@@ -420,7 +473,7 @@
selection = TextRange(newCursorOffset, newCursorOffset)
)
onValueChange(newValue)
- setSelectionStatus(false)
+ setHandleState(HandleState.None)
}
/**
@@ -445,7 +498,7 @@
selection = TextRange(newCursorOffset, newCursorOffset)
)
onValueChange(newValue)
- setSelectionStatus(false)
+ setHandleState(HandleState.None)
undoManager?.forceNextSnapshot()
}
@@ -473,13 +526,13 @@
selection = TextRange(newCursorOffset, newCursorOffset)
)
onValueChange(newValue)
- setSelectionStatus(false)
+ setHandleState(HandleState.None)
undoManager?.forceNextSnapshot()
}
/*@VisibleForTesting*/
internal fun selectAll() {
- setSelectionStatus(true)
+ setHandleState(HandleState.None)
val newValue = createTextFieldValue(
annotatedString = value.annotatedString,
@@ -653,10 +706,8 @@
state?.showSelectionHandleEnd = isSelectionHandleInVisibleBound(false)
}
- private fun setSelectionStatus(on: Boolean) {
- state?.let {
- it.selectionIsOn = on
- }
+ private fun setHandleState(handleState: HandleState) {
+ state?.let { it.handleState = handleState }
}
private fun createTextFieldValue(
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%
copy from benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt
copy 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/desktopMain/kotlin/androidx/compose/foundation/text/DesktopCursorHandle.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DesktopCursorHandle.desktop.kt
new file mode 100644
index 0000000..cc0556f
--- /dev/null
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DesktopCursorHandle.desktop.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.compose.foundation.text
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+
+@Composable
+@Suppress("UNUSED_PARAMETER")
+internal actual fun CursorHandle(
+ handlePosition: Offset,
+ modifier: Modifier,
+ content: @Composable (() -> Unit)?
+) {
+ /* Not implemented. */
+}
\ 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/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
index 878059e..01ce392c 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
@@ -17,6 +17,7 @@
package androidx.compose.foundation.text.selection
import androidx.compose.foundation.text.InternalFoundationTextApi
+import androidx.compose.foundation.text.HandleState
import androidx.compose.foundation.text.TextFieldState
import androidx.compose.foundation.text.TextLayoutResultProxy
import androidx.compose.ui.focus.FocusRequester
@@ -150,7 +151,7 @@
manager.touchSelectionObserver.onStart(dragBeginPosition)
- assertThat(state.selectionIsOn).isTrue()
+ assertThat(state.handleState).isEqualTo(HandleState.Selection)
assertThat(state.showFloatingToolbar).isTrue()
assertThat(value.selection).isEqualTo(fakeTextRange)
verify(
@@ -180,7 +181,7 @@
manager.touchSelectionObserver.onStart(dragBeginPosition)
// Assert
- assertThat(state.selectionIsOn).isTrue()
+ assertThat(state.handleState).isEqualTo(HandleState.Selection)
assertThat(state.showFloatingToolbar).isTrue()
assertThat(value.selection).isEqualTo(TextRange(fakeLineEnd))
verify(
@@ -287,6 +288,48 @@
}
@Test
+ fun TextFieldSelectionManager_cursorDragObserver_onStart() {
+ manager.cursorDragObserver().onStart(Offset.Zero)
+
+ assertThat(state.draggingHandle).isTrue()
+ assertThat(state.showFloatingToolbar).isFalse()
+ verify(spyLambda, times(0)).invoke(any())
+ verify(
+ hapticFeedback,
+ times(0)
+ ).performHapticFeedback(HapticFeedbackType.TextHandleMove)
+ }
+
+ @Test
+ fun TextFieldSelectionManager_cursorDragObserver_onDrag() {
+ manager.value = TextFieldValue(text = text, selection = TextRange(0, "Hello".length))
+
+ manager.cursorDragObserver().onDrag(dragDistance)
+
+ assertThat(state.showFloatingToolbar).isFalse()
+ assertThat(value.selection).isEqualTo(TextRange(dragOffset, dragOffset))
+ verify(
+ hapticFeedback,
+ times(1)
+ ).performHapticFeedback(HapticFeedbackType.TextHandleMove)
+ }
+
+ @Test
+ fun TextFieldSelectionManager_cursorDragObserver_onStop() {
+ manager.handleDragObserver(false).onStart(Offset.Zero)
+ manager.handleDragObserver(false).onDrag(Offset.Zero)
+
+ manager.cursorDragObserver().onStop()
+
+ assertThat(state.draggingHandle).isFalse()
+ assertThat(state.showFloatingToolbar).isFalse()
+ verify(
+ hapticFeedback,
+ times(0)
+ ).performHapticFeedback(HapticFeedbackType.TextHandleMove)
+ }
+
+ @Test
fun TextFieldSelectionManager_deselect() {
whenever(textToolbar.status).thenReturn(TextToolbarStatus.Shown)
manager.value = TextFieldValue(text = text, selection = TextRange(0, "Hello".length))
@@ -295,7 +338,7 @@
verify(textToolbar, times(1)).hide()
assertThat(value.selection).isEqualTo(TextRange("Hello".length))
- assertThat(state.selectionIsOn).isFalse()
+ assertThat(state.handleState).isEqualTo(HandleState.None)
}
@Test
@@ -315,7 +358,7 @@
verify(clipboardManager, times(1)).setText(AnnotatedString("Hello"))
assertThat(value.selection).isEqualTo(TextRange("Hello".length, "Hello".length))
- assertThat(state.selectionIsOn).isFalse()
+ assertThat(state.handleState).isEqualTo(HandleState.None)
}
@Test
@@ -329,7 +372,7 @@
verify(clipboardManager, times(1)).setText(AnnotatedString("llo"))
assertThat(value.selection).isEqualTo(TextRange("Hello".length, "Hello".length))
- assertThat(state.selectionIsOn).isFalse()
+ assertThat(state.handleState).isEqualTo(HandleState.None)
}
@Test
@@ -362,7 +405,7 @@
assertThat(value.text).isEqualTo("HelHellorld")
assertThat(value.selection).isEqualTo(TextRange("Hello Wo".length, "Hello Wo".length))
- assertThat(state.selectionIsOn).isFalse()
+ assertThat(state.handleState).isEqualTo(HandleState.None)
}
@Test
@@ -377,7 +420,7 @@
assertThat(value.text).isEqualTo("Hi World")
assertThat(value.selection).isEqualTo(TextRange("Hi".length, "Hi".length))
- assertThat(state.selectionIsOn).isFalse()
+ assertThat(state.handleState).isEqualTo(HandleState.None)
}
@Test
@@ -401,7 +444,7 @@
verify(clipboardManager, times(1)).setText(AnnotatedString(" World"))
assertThat(value.text).isEqualTo("HelloHello World")
assertThat(value.selection).isEqualTo(TextRange("Hello".length, "Hello".length))
- assertThat(state.selectionIsOn).isFalse()
+ assertThat(state.handleState).isEqualTo(HandleState.None)
}
@Test
@@ -416,7 +459,7 @@
verify(clipboardManager, times(1)).setText(AnnotatedString("llo"))
assertThat(value.text).isEqualTo("He World")
assertThat(value.selection).isEqualTo(TextRange("He".length, "He".length))
- assertThat(state.selectionIsOn).isFalse()
+ assertThat(state.handleState).isEqualTo(HandleState.None)
}
@Test
diff --git a/compose/lint/common-test/src/main/java/androidx/compose/lint/test/Stubs.kt b/compose/lint/common-test/src/main/java/androidx/compose/lint/test/Stubs.kt
index d198858..2169a8f 100644
--- a/compose/lint/common-test/src/main/java/androidx/compose/lint/test/Stubs.kt
+++ b/compose/lint/common-test/src/main/java/androidx/compose/lint/test/Stubs.kt
@@ -525,6 +525,7 @@
): KotlinAndCompiledStub {
val filenameWithoutExtension = filename.substringBefore(".").lowercase(Locale.ROOT)
val kotlin = kotlin(source).to("$filepath/$filename")
+ @Suppress("DEPRECATION") // b/193244821
val compiled = compiled(
"libs/$filenameWithoutExtension.jar",
kotlin,
diff --git a/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/ModifierInspectorInfoDetectorTest.kt b/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/ModifierInspectorInfoDetectorTest.kt
index 95b8a38..3485e15 100644
--- a/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/ModifierInspectorInfoDetectorTest.kt
+++ b/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/ModifierInspectorInfoDetectorTest.kt
@@ -22,6 +22,7 @@
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -564,6 +565,7 @@
.expectClean()
}
+ @Ignore // b/193270279
@Test
fun rememberModifierInfo() {
lint().files(
@@ -602,10 +604,12 @@
.expectClean()
}
+ @Ignore // b/193270279
@Test
fun emptyModifier() {
lint().files(
Stubs.Modifier,
+ Stubs.Remember,
composedStub,
kotlin(
"""
diff --git a/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/UnnecessaryLambdaCreationDetectorTest.kt b/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/UnnecessaryLambdaCreationDetectorTest.kt
index 80bc35f..1332499 100644
--- a/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/UnnecessaryLambdaCreationDetectorTest.kt
+++ b/compose/lint/internal-lint-checks/src/test/java/androidx/compose/lint/UnnecessaryLambdaCreationDetectorTest.kt
@@ -28,6 +28,7 @@
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.intellij.lang.annotations.Language
+import org.junit.Ignore
import org.junit.runners.Parameterized
/* ktlint-disable max-line-length */
@@ -110,6 +111,7 @@
.run()
}
+ @Ignore // b/193270279
@Test
fun warnsForSingleExpressions() {
check(
diff --git a/compose/material/material-lint/src/test/java/androidx/compose/material/lint/ColorsDetectorTest.kt b/compose/material/material-lint/src/test/java/androidx/compose/material/lint/ColorsDetectorTest.kt
index f2d315e..34e94c9 100644
--- a/compose/material/material-lint/src/test/java/androidx/compose/material/lint/ColorsDetectorTest.kt
+++ b/compose/material/material-lint/src/test/java/androidx/compose/material/lint/ColorsDetectorTest.kt
@@ -23,6 +23,7 @@
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -590,6 +591,7 @@
.expectClean()
}
+ @Ignore // b/193270279
@Test
fun lightColorsErrors_compiled() {
lint().files(
@@ -629,6 +631,7 @@
)
}
+ @Ignore // b/193270279
@Test
fun darkColorsErrors_compiled() {
lint().files(
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index e86986a..6c6bfd5 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -204,6 +204,9 @@
method @androidx.compose.runtime.Composable public static void Divider(optional androidx.compose.ui.Modifier modifier, optional long color, optional float thickness, optional float startIndent);
}
+ public final class DragGestureDetectorCopyKt {
+ }
+
public final class DrawerDefaults {
method public float getElevation();
method @androidx.compose.runtime.Composable public long getScrimColor();
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index e1e3e04..3b0ae6f 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> {
@@ -301,6 +302,9 @@
method @androidx.compose.runtime.Composable public static void Divider(optional androidx.compose.ui.Modifier modifier, optional long color, optional float thickness, optional float startIndent);
}
+ public final class DragGestureDetectorCopyKt {
+ }
+
public final class DrawerDefaults {
method public float getElevation();
method @androidx.compose.runtime.Composable public long getScrimColor();
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index e86986a..6c6bfd5 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -204,6 +204,9 @@
method @androidx.compose.runtime.Composable public static void Divider(optional androidx.compose.ui.Modifier modifier, optional long color, optional float thickness, optional float startIndent);
}
+ public final class DragGestureDetectorCopyKt {
+ }
+
public final class DrawerDefaults {
method public float getElevation();
method @androidx.compose.runtime.Composable public long getScrimColor();
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/DragGestureDetectorCopy.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/DragGestureDetectorCopy.kt
new file mode 100644
index 0000000..2bd724d
--- /dev/null
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/DragGestureDetectorCopy.kt
@@ -0,0 +1,115 @@
+/*
+ * 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
+
+// Copy-paste version of DragGestureDetector.kt. Please don't change this file without changing
+// DragGestureDetector.kt
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.AwaitPointerEventScope
+import androidx.compose.ui.input.pointer.PointerEvent
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerId
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerType
+import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
+import androidx.compose.ui.input.pointer.positionChangeConsumed
+import androidx.compose.ui.platform.ViewConfiguration
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastFirstOrNull
+import kotlin.math.abs
+import kotlin.math.sign
+
+internal suspend fun AwaitPointerEventScope.awaitHorizontalPointerSlopOrCancellation(
+ pointerId: PointerId,
+ pointerType: PointerType,
+ onPointerSlopReached: (change: PointerInputChange, overSlop: Float) -> Unit
+) = awaitPointerSlopOrCancellation(
+ pointerId = pointerId,
+ pointerType = pointerType,
+ onPointerSlopReached = onPointerSlopReached,
+ getDragDirectionValue = { it.x }
+)
+
+private suspend inline fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
+ pointerId: PointerId,
+ pointerType: PointerType,
+ onPointerSlopReached: (PointerInputChange, Float) -> Unit,
+ getDragDirectionValue: (Offset) -> Float
+): PointerInputChange? {
+ if (currentEvent.isPointerUp(pointerId)) {
+ return null // The pointer has already been lifted, so the gesture is canceled
+ }
+ val touchSlop = viewConfiguration.pointerSlop(pointerType)
+ var pointer: PointerId = pointerId
+ var totalPositionChange = 0f
+
+ while (true) {
+ val event = awaitPointerEvent()
+ val dragEvent = event.changes.fastFirstOrNull { it.id == pointer }!!
+ if (dragEvent.positionChangeConsumed()) {
+ return null
+ } else if (dragEvent.changedToUpIgnoreConsumed()) {
+ val otherDown = event.changes.fastFirstOrNull { it.pressed }
+ if (otherDown == null) {
+ // This is the last "up"
+ return null
+ } else {
+ pointer = otherDown.id
+ }
+ } else {
+ val currentPosition = dragEvent.position
+ val previousPosition = dragEvent.previousPosition
+ val positionChange = getDragDirectionValue(currentPosition) -
+ getDragDirectionValue(previousPosition)
+ totalPositionChange += positionChange
+
+ val inDirection = abs(totalPositionChange)
+ if (inDirection < touchSlop) {
+ // verify that nothing else consumed the drag event
+ awaitPointerEvent(PointerEventPass.Final)
+ if (dragEvent.positionChangeConsumed()) {
+ return null
+ }
+ } else {
+ onPointerSlopReached(
+ dragEvent,
+ totalPositionChange - (sign(totalPositionChange) * touchSlop)
+ )
+ if (dragEvent.positionChangeConsumed()) {
+ return dragEvent
+ } else {
+ totalPositionChange = 0f
+ }
+ }
+ }
+ }
+}
+
+private fun PointerEvent.isPointerUp(pointerId: PointerId): Boolean =
+ changes.fastFirstOrNull { it.id == pointerId }?.pressed != true
+
+private val mouseSlop = 0.125.dp
+private val defaultTouchSlop = 18.dp // The default touch slop on Android devices
+private val mouseToTouchSlopRatio = mouseSlop / defaultTouchSlop
+
+internal fun ViewConfiguration.pointerSlop(pointerType: PointerType): Float {
+ return when (pointerType) {
+ PointerType.Mouse -> touchSlop * mouseToTouchSlopRatio
+ else -> touchSlop
+ }
+}
\ No newline at end of file
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/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
index f819ae4..3f46f2f 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
@@ -27,7 +27,6 @@
import androidx.compose.foundation.gestures.DraggableState
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.awaitFirstDown
-import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.forEachGesture
@@ -75,6 +74,7 @@
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.input.pointer.consumePositionChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
@@ -703,14 +703,15 @@
}
private suspend fun AwaitPointerEventScope.awaitSlop(
- id: PointerId
+ id: PointerId,
+ type: PointerType
): Pair<PointerInputChange, Float>? {
var initialDelta = 0f
- val postTouchSlop = { pointerInput: PointerInputChange, offset: Float ->
+ val postPointerSlop = { pointerInput: PointerInputChange, offset: Float ->
pointerInput.consumePositionChange()
initialDelta = offset
}
- val afterSlopResult = awaitHorizontalTouchSlopOrCancellation(id, postTouchSlop)
+ val afterSlopResult = awaitHorizontalPointerSlopOrCancellation(id, type, postPointerSlop)
return if (afterSlopResult != null) afterSlopResult to initialDelta else null
}
@@ -862,7 +863,7 @@
var draggingStart = true
val pointerEvent = awaitFirstDown(requireUnconsumed = false)
val interaction = PressInteraction.Press(pointerEvent.position)
- val slop = viewConfiguration.touchSlop
+ val slop = viewConfiguration.pointerSlop(pointerEvent.type)
val posX =
if (isRtl) maxPx - pointerEvent.position.x else pointerEvent.position.x
@@ -880,7 +881,7 @@
thumbCaptured = true
}
- awaitSlop(pointerEvent.id)?.let {
+ awaitSlop(pointerEvent.id, pointerEvent.type)?.let {
if (thumbCaptured) {
onDrag(draggingStart, if (isRtl) -it.second else it.second)
} else {
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableCoroutineCreationDetectorTest.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableCoroutineCreationDetectorTest.kt
index bd3e55c..710a310 100644
--- a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableCoroutineCreationDetectorTest.kt
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableCoroutineCreationDetectorTest.kt
@@ -24,6 +24,7 @@
import com.android.tools.lint.checks.infrastructure.TestFile
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -97,6 +98,7 @@
"""
)
+ @Ignore // b/193270279
@Test
fun errors() {
lint().files(
@@ -204,6 +206,7 @@
)
}
+ @Ignore // b/193270279
@Test
fun noErrors() {
lint().files(
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableFlowOperatorDetectorTest.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableFlowOperatorDetectorTest.kt
index ef2161cd..83bafd2 100644
--- a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableFlowOperatorDetectorTest.kt
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableFlowOperatorDetectorTest.kt
@@ -24,6 +24,7 @@
import com.android.tools.lint.checks.infrastructure.TestFile
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -115,6 +116,7 @@
"""
)
+ @Ignore // b/193270279
@Test
fun errors() {
lint().files(
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/CompositionLocalNamingDetectorTest.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/CompositionLocalNamingDetectorTest.kt
index 9413580..4ee844d 100644
--- a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/CompositionLocalNamingDetectorTest.kt
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/CompositionLocalNamingDetectorTest.kt
@@ -22,6 +22,7 @@
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -162,6 +163,7 @@
"""
)
+ @Ignore // b/193270279
@Test
fun noLocalPrefix() {
lint().files(
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/RememberDetectorTest.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/RememberDetectorTest.kt
index 4fa7427..e2b7f66 100644
--- a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/RememberDetectorTest.kt
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/RememberDetectorTest.kt
@@ -22,6 +22,7 @@
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -38,6 +39,7 @@
override fun getIssues(): MutableList<Issue> =
mutableListOf(RememberDetector.RememberReturnType)
+ @Ignore // b/193270279
@Test
fun returnsUnit() {
lint().files(
@@ -159,6 +161,7 @@
)
}
+ @Ignore // b/193270279
@Test
fun returnsValue_explicitUnitType() {
lint().files(
@@ -280,6 +283,7 @@
)
}
+ @Ignore // b/193270279
@Test
fun noErrors() {
lint().files(
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/UnrememberedMutableStateDetectorTest.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/UnrememberedMutableStateDetectorTest.kt
index 1a307c6..ea7f9f5 100644
--- a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/UnrememberedMutableStateDetectorTest.kt
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/UnrememberedMutableStateDetectorTest.kt
@@ -22,6 +22,7 @@
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -38,6 +39,7 @@
override fun getIssues(): MutableList<Issue> =
mutableListOf(UnrememberedMutableStateDetector.UnrememberedMutableState)
+ @Ignore // b/193270279
@Test
fun notRemembered() {
lint().files(
@@ -191,6 +193,7 @@
)
}
+ @Ignore // b/193270279
@Test
fun rememberedInsideComposableBody() {
lint().files(
@@ -280,6 +283,7 @@
.expectClean()
}
+ @Ignore // b/193270279
@Test
fun noErrors() {
lint().files(
diff --git a/compose/runtime/runtime-saveable-lint/src/test/java/androidx/compose/runtime/saveable/lint/RememberSaveableDetectorTest.kt b/compose/runtime/runtime-saveable-lint/src/test/java/androidx/compose/runtime/saveable/lint/RememberSaveableDetectorTest.kt
index 3432370..2fadca1 100644
--- a/compose/runtime/runtime-saveable-lint/src/test/java/androidx/compose/runtime/saveable/lint/RememberSaveableDetectorTest.kt
+++ b/compose/runtime/runtime-saveable-lint/src/test/java/androidx/compose/runtime/saveable/lint/RememberSaveableDetectorTest.kt
@@ -24,6 +24,7 @@
import com.android.tools.lint.checks.infrastructure.TestFile
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -130,6 +131,7 @@
"""
)
+ @Ignore // b/193270279
@Test
fun saverPassedToVarargs() {
lint().files(
@@ -231,6 +233,7 @@
)
}
+ @Ignore // b/193270279
@Test
fun noErrors() {
lint().files(
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/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index d676337..8095df7 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -1441,18 +1441,6 @@
}
}
- // Update the transparent snapshot if necessary
- // This doesn't need to take the sync because it is updating thread local state.
- (threadSnapshot.get() as? TransparentObserverMutableSnapshot)?.let {
- threadSnapshot.set(
- TransparentObserverMutableSnapshot(
- currentGlobalSnapshot.get(),
- it.specifiedReadObserver,
- it.specifiedWriteObserver
- )
- )
- it.dispose()
- }
return result
}
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 3448a42..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)
+ }
}
}
@@ -113,8 +115,10 @@
if (!isObserving) {
isObserving = true
try {
- applyMap.map.removeValueIf {
- it === scope
+ synchronized(applyMaps) {
+ applyMap.map.removeValueIf {
+ it === scope
+ }
}
Snapshot.observe(readObserver, null, block)
} finally {
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 2d7e1ec..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
@@ -17,9 +17,13 @@
package androidx.compose.runtime.snapshots
import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import kotlin.concurrent.thread
import kotlin.test.Test
import kotlin.test.assertEquals
+import kotlin.test.assertNull
class SnapshotStateObserverTests {
@@ -282,6 +286,82 @@
assertEquals(1, changes2)
}
+ @Test // regression test for 192677711
+ fun tryToReproduceRaceCondition() {
+ 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()
+ }
+ repeat(1000) {
+ observer.observeReads(Unit, {}) {
+ @Suppress("UNUSED_EXPRESSION")
+ state1
+ if (state2) {
+ state2 = false
+ }
+ }
+ }
+ assertNull(threadException)
+ }
+ } finally {
+ running = false
+ }
+ 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/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
index 5ce7ca3..6ec7593 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
@@ -703,6 +703,27 @@
nestedChild.dispose()
}
+ @Test // Regression test for b/193006595
+ fun transparentSnapshotAdvancesCorrectly() {
+ val state = Snapshot.observe({}) {
+ // In a transparent snapshot, advance the global snapshot
+ Snapshot.notifyObjectsInitialized()
+
+ // Create an apply an object in a snapshot
+ val state = atomic {
+ mutableStateOf(0)
+ }
+
+ // Ensure that the object can be accessed in the observer
+ assertEquals(0, state.value)
+
+ state
+ }
+
+ // Ensure that the object can be accessed globally.
+ assertEquals(0, state.value)
+ }
+
private var count = 0
@BeforeTest
diff --git a/compose/ui/ui-graphics-lint/src/test/java/androidx/compose/ui/graphics/lint/ColorDetectorTest.kt b/compose/ui/ui-graphics-lint/src/test/java/androidx/compose/ui/graphics/lint/ColorDetectorTest.kt
index c12d4b1..4ba5cae 100644
--- a/compose/ui/ui-graphics-lint/src/test/java/androidx/compose/ui/graphics/lint/ColorDetectorTest.kt
+++ b/compose/ui/ui-graphics-lint/src/test/java/androidx/compose/ui/graphics/lint/ColorDetectorTest.kt
@@ -22,6 +22,7 @@
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -41,6 +42,7 @@
ColorDetector.InvalidColorHexValue
)
+ @Ignore // b/193270279
@Test
fun MissingColorAlphaChannel() {
lint().files(
@@ -110,6 +112,7 @@
)
}
+ @Ignore // b/193270279
@Test
fun incorrectChannels() {
lint().files(
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..c8fcfa2 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)
@@ -297,7 +297,7 @@
0f,
width.toFloat(),
height.toFloat(),
- android.graphics.Paint().apply {
+ frameworkPaint().apply {
isAntiAlias = true
color = android.graphics.Color.BLUE
pathEffect = android.graphics.CornerPathEffect(radius)
@@ -305,12 +305,12 @@
)
val composePixels = imageBitmap.toPixelMap()
- for (i in 0 until 80) {
- for (j in 0 until 80) {
+ for (i in 0 until width) {
+ for (j in 0 until height) {
assertEquals(
"invalid color at i: " + i + ", " + j,
- composePixels[i, j].toArgb(),
- androidBitmap.getPixel(i, j)
+ composePixels[i, j],
+ Color(androidBitmap.getPixel(i, j)),
)
}
}
@@ -342,7 +342,7 @@
0f,
width.toFloat(),
height.toFloat(),
- android.graphics.Paint().apply {
+ frameworkPaint().apply {
isAntiAlias = true
color = android.graphics.Color.BLUE
pathEffect = android.graphics.DashPathEffect(floatArrayOf(10f, 5f), 8f)
@@ -391,7 +391,7 @@
0f,
width.toFloat(),
height.toFloat(),
- android.graphics.Paint().apply {
+ frameworkPaint().apply {
isAntiAlias = true
color = android.graphics.Color.BLUE
pathEffect =
@@ -450,7 +450,7 @@
0f,
width.toFloat(),
height.toFloat(),
- android.graphics.Paint().apply {
+ frameworkPaint().apply {
isAntiAlias = true
color = android.graphics.Color.BLUE
pathEffect =
@@ -505,7 +505,7 @@
0f,
width.toFloat(),
height.toFloat(),
- android.graphics.Paint().apply {
+ frameworkPaint().apply {
isAntiAlias = true
color = android.graphics.Color.BLUE
colorFilter = PorterDuffColorFilter(Color.Magenta.toArgb(), PorterDuff.Mode.SRC_IN)
@@ -550,7 +550,7 @@
0f,
width.toFloat(),
height.toFloat(),
- android.graphics.Paint().apply {
+ frameworkPaint().apply {
isAntiAlias = true
color = android.graphics.Color.BLUE
colorFilter = LightingColorFilter(Color.Red.toArgb(), Color.Blue.toArgb())
@@ -597,7 +597,7 @@
0f,
width.toFloat(),
height.toFloat(),
- android.graphics.Paint().apply {
+ frameworkPaint().apply {
isAntiAlias = true
color = android.graphics.Color.BLUE
colorFilter = ColorMatrixColorFilter(colorMatrix.values)
@@ -616,6 +616,13 @@
}
}
+ fun frameworkPaint(): android.graphics.Paint =
+ android.graphics.Paint(
+ android.graphics.Paint.ANTI_ALIAS_FLAG or
+ android.graphics.Paint.DITHER_FLAG or
+ android.graphics.Paint.FILTER_BITMAP_FLAG
+ )
+
class EnableDisableZViewGroup @JvmOverloads constructor(
val drawLatch: CountDownLatch,
context: Context,
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PaintTest.kt b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PaintTest.kt
index 85e3f89..bce96d7 100644
--- a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PaintTest.kt
+++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PaintTest.kt
@@ -50,4 +50,14 @@
paint.pathEffect = pathEffect
assertTrue(pathEffect === paint.pathEffect)
}
+
+ @Test
+ fun testDitheringEnabledByDefault() {
+ assertTrue(Paint().asFrameworkPaint().isDither)
+ }
+
+ @Test
+ fun testFilterBitmapEnabledByDefault() {
+ assertTrue(Paint().asFrameworkPaint().isFilterBitmap)
+ }
}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPaint.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPaint.android.kt
index 0c8a2b5..55164ab 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPaint.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPaint.android.kt
@@ -117,7 +117,11 @@
}
internal fun makeNativePaint() =
- android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG)
+ android.graphics.Paint(
+ android.graphics.Paint.ANTI_ALIAS_FLAG or
+ android.graphics.Paint.DITHER_FLAG or
+ android.graphics.Paint.FILTER_BITMAP_FLAG
+ )
internal fun NativePaint.setNativeBlendMode(mode: BlendMode) {
if (Build.VERSION.SDK_INT >= 29) {
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-lint/src/test/java/androidx/compose/ui/lint/ComposedModifierDetectorTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ComposedModifierDetectorTest.kt
index 84df348..00a46f9 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ComposedModifierDetectorTest.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ComposedModifierDetectorTest.kt
@@ -23,6 +23,7 @@
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -104,6 +105,7 @@
"""
)
+ @Ignore // b/193270279
@Test
fun noComposableCalls() {
lint().files(
@@ -226,6 +228,7 @@
Stubs.Modifier,
Stubs.Remember
)
+ .allowCompilationErrors(true) // b/193270279
.run()
.expectClean()
}
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index dbb8403..4ec1a37 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -579,6 +579,10 @@
method @androidx.compose.runtime.Composable public static void Path(java.util.List<? extends androidx.compose.ui.graphics.vector.PathNode> pathData, optional int pathFillType, optional String name, optional androidx.compose.ui.graphics.Brush? fill, optional float fillAlpha, optional androidx.compose.ui.graphics.Brush? stroke, optional float strokeAlpha, optional float strokeLineWidth, optional int strokeLineCap, optional int strokeLineJoin, optional float strokeLineMiter, optional float trimPathStart, optional float trimPathEnd, optional float trimPathOffset);
}
+ @androidx.compose.ui.ExperimentalComposeUiApi public interface VectorConfig {
+ method public default <T> T! getOrDefault(androidx.compose.ui.graphics.vector.VectorProperty<T> property, T? defaultValue);
+ }
+
@androidx.compose.runtime.Immutable public final class VectorGroup extends androidx.compose.ui.graphics.vector.VectorNode implements java.lang.Iterable<androidx.compose.ui.graphics.vector.VectorNode> kotlin.jvm.internal.markers.KMappedMarker {
method public operator androidx.compose.ui.graphics.vector.VectorNode get(int index);
method public java.util.List<androidx.compose.ui.graphics.vector.PathNode> getClipPathData();
@@ -639,6 +643,7 @@
}
public final class VectorPainterKt {
+ method @androidx.compose.runtime.Composable @androidx.compose.ui.ExperimentalComposeUiApi public static void RenderVectorGroup(androidx.compose.ui.graphics.vector.VectorGroup group, optional java.util.Map<java.lang.String,? extends androidx.compose.ui.graphics.vector.VectorConfig> configs);
method @androidx.compose.runtime.Composable public static androidx.compose.ui.graphics.vector.VectorPainter rememberVectorPainter(float defaultWidth, float defaultHeight, optional float viewportWidth, optional float viewportHeight, optional String name, optional long tintColor, optional int tintBlendMode, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static androidx.compose.ui.graphics.vector.VectorPainter rememberVectorPainter(androidx.compose.ui.graphics.vector.ImageVector image);
field public static final String RootGroupName = "VectorRootGroup";
@@ -675,6 +680,73 @@
property public final float trimPathStart;
}
+ @androidx.compose.ui.ExperimentalComposeUiApi public abstract sealed class VectorProperty<T> {
+ }
+
+ public static final class VectorProperty.Fill extends androidx.compose.ui.graphics.vector.VectorProperty<androidx.compose.ui.graphics.Brush> {
+ field public static final androidx.compose.ui.graphics.vector.VectorProperty.Fill INSTANCE;
+ }
+
+ public static final class VectorProperty.FillAlpha extends androidx.compose.ui.graphics.vector.VectorProperty<java.lang.Float> {
+ field public static final androidx.compose.ui.graphics.vector.VectorProperty.FillAlpha INSTANCE;
+ }
+
+ public static final class VectorProperty.PathData extends androidx.compose.ui.graphics.vector.VectorProperty<java.util.List<? extends androidx.compose.ui.graphics.vector.PathNode>> {
+ field public static final androidx.compose.ui.graphics.vector.VectorProperty.PathData INSTANCE;
+ }
+
+ public static final class VectorProperty.PivotX extends androidx.compose.ui.graphics.vector.VectorProperty<java.lang.Float> {
+ field public static final androidx.compose.ui.graphics.vector.VectorProperty.PivotX INSTANCE;
+ }
+
+ public static final class VectorProperty.PivotY extends androidx.compose.ui.graphics.vector.VectorProperty<java.lang.Float> {
+ field public static final androidx.compose.ui.graphics.vector.VectorProperty.PivotY INSTANCE;
+ }
+
+ public static final class VectorProperty.Rotation extends androidx.compose.ui.graphics.vector.VectorProperty<java.lang.Float> {
+ field public static final androidx.compose.ui.graphics.vector.VectorProperty.Rotation INSTANCE;
+ }
+
+ public static final class VectorProperty.ScaleX extends androidx.compose.ui.graphics.vector.VectorProperty<java.lang.Float> {
+ field public static final androidx.compose.ui.graphics.vector.VectorProperty.ScaleX INSTANCE;
+ }
+
+ public static final class VectorProperty.ScaleY extends androidx.compose.ui.graphics.vector.VectorProperty<java.lang.Float> {
+ field public static final androidx.compose.ui.graphics.vector.VectorProperty.ScaleY INSTANCE;
+ }
+
+ public static final class VectorProperty.Stroke extends androidx.compose.ui.graphics.vector.VectorProperty<androidx.compose.ui.graphics.Brush> {
+ field public static final androidx.compose.ui.graphics.vector.VectorProperty.Stroke INSTANCE;
+ }
+
+ public static final class VectorProperty.StrokeAlpha extends androidx.compose.ui.graphics.vector.VectorProperty<java.lang.Float> {
+ field public static final androidx.compose.ui.graphics.vector.VectorProperty.StrokeAlpha INSTANCE;
+ }
+
+ public static final class VectorProperty.StrokeLineWidth extends androidx.compose.ui.graphics.vector.VectorProperty<java.lang.Float> {
+ field public static final androidx.compose.ui.graphics.vector.VectorProperty.StrokeLineWidth INSTANCE;
+ }
+
+ public static final class VectorProperty.TranslateX extends androidx.compose.ui.graphics.vector.VectorProperty<java.lang.Float> {
+ field public static final androidx.compose.ui.graphics.vector.VectorProperty.TranslateX INSTANCE;
+ }
+
+ public static final class VectorProperty.TranslateY extends androidx.compose.ui.graphics.vector.VectorProperty<java.lang.Float> {
+ field public static final androidx.compose.ui.graphics.vector.VectorProperty.TranslateY INSTANCE;
+ }
+
+ public static final class VectorProperty.TrimPathEnd extends androidx.compose.ui.graphics.vector.VectorProperty<java.lang.Float> {
+ field public static final androidx.compose.ui.graphics.vector.VectorProperty.TrimPathEnd INSTANCE;
+ }
+
+ public static final class VectorProperty.TrimPathOffset extends androidx.compose.ui.graphics.vector.VectorProperty<java.lang.Float> {
+ field public static final androidx.compose.ui.graphics.vector.VectorProperty.TrimPathOffset INSTANCE;
+ }
+
+ public static final class VectorProperty.TrimPathStart extends androidx.compose.ui.graphics.vector.VectorProperty<java.lang.Float> {
+ field public static final androidx.compose.ui.graphics.vector.VectorProperty.TrimPathStart INSTANCE;
+ }
+
}
package androidx.compose.ui.graphics.vector.compat {
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index e8887a6..c56710f 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -250,9 +250,9 @@
def verifyKotlinModule(String variant) {
project.afterEvaluate {
def capitalVariant = variant.capitalize()
+ def moduleFile = new File("${buildDir}/tmp/kotlin-classes/${variant}/META-INF/ui_${variant}.kotlin_module")
tasks.named("compile${capitalVariant}Kotlin").configure { t ->
t.doLast {
- def moduleFile = project.file("${buildDir}/tmp/kotlin-classes/${variant}/META-INF/ui_${variant}.kotlin_module")
// This file should be large, about 3.2K. If this file is short then many symbols will fail to resolve
if (moduleFile.length() < 250) {
throw new GradleException("kotlin_module file ($moduleFile) too short! See b/188565660 for more information. File text: ${moduleFile.text}")
diff --git a/compose/ui/ui/src/androidAndroidTest/AndroidManifest.xml b/compose/ui/ui/src/androidAndroidTest/AndroidManifest.xml
index 5a120c7..d62742c 100644
--- a/compose/ui/ui/src/androidAndroidTest/AndroidManifest.xml
+++ b/compose/ui/ui/src/androidAndroidTest/AndroidManifest.xml
@@ -29,5 +29,8 @@
<activity android:name="androidx.fragment.app.FragmentActivity"/>
<activity android:name="androidx.compose.ui.window.ActivityWithFlagSecure"/>
<activity android:name="androidx.compose.ui.RecyclerViewActivity" />
+ <activity
+ android:name="androidx.compose.ui.draw.NotHardwareAcceleratedActivity"
+ android:hardwareAccelerated="false" />
</application>
</manifest>
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/androidAndroidTest/kotlin/androidx/compose/ui/draw/NotHardwareAcceleratedActivityTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/NotHardwareAcceleratedActivityTest.kt
new file mode 100644
index 0000000..7657dec2
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/NotHardwareAcceleratedActivityTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.draw
+
+import android.os.Build
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.GOLDEN_UI
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.background
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+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
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@RunWith(AndroidJUnit4::class)
+class NotHardwareAcceleratedActivityTest {
+
+ @get:Rule
+ val composeTestRule = createAndroidComposeRule<NotHardwareAcceleratedActivity>()
+
+ @get:Rule
+ val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_UI)
+
+ @Test
+ fun layerTransformationsApplied() {
+ composeTestRule.setContent {
+ Box(Modifier.size(200.dp).background(Color.White).testTag("box")) {
+ Box(
+ Modifier
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ val offset = 50.dp.roundToPx()
+ placeable.placeWithLayer(offset, offset) {
+ alpha = 0.5f
+ scaleX = 0.5f
+ scaleY = 0.5f
+ }
+ }
+ }
+ .size(100.dp)
+ .background(Color.Red)
+ )
+ }
+ }
+
+ composeTestRule.onNodeWithTag("box")
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, "not_hardware_accelerated_activity")
+ }
+}
+
+class NotHardwareAcceleratedActivity : ComponentActivity()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
index c476a98..acb37cc 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
@@ -520,17 +520,18 @@
composeView.setPadding(padding, padding, padding, padding)
rule.activity.setContentView(composeView)
- val position = IntArray(2)
- composeView.getLocationInWindow(position)
- frameGlobalPosition = Offset(position[0].toFloat(), position[1].toFloat())
-
composeView.setContent {
Box(
Modifier.fillMaxSize().onGloballyPositioned {
+ val position = IntArray(2)
+ composeView.getLocationInWindow(position)
+ frameGlobalPosition = Offset(position[0].toFloat(), position[1].toFloat())
+
realGlobalPosition = it.localToWindow(localPosition)
realLocalPosition = it.windowToLocal(
framePadding + frameGlobalPosition!!
)
+
positionedLatch.countDown()
}
)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/graphics/vector/compat/AndroidVectorResources.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/graphics/vector/compat/AndroidVectorResources.android.kt
index 7414d68..1c1e21c 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/graphics/vector/compat/AndroidVectorResources.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/graphics/vector/compat/AndroidVectorResources.android.kt
@@ -95,86 +95,4 @@
intArrayOf(android.R.attr.name, android.R.attr.pathData)
val STYLEABLE_VECTOR_DRAWABLE_CLIP_PATH_NAME = 0
val STYLEABLE_VECTOR_DRAWABLE_CLIP_PATH_PATH_DATA = 1
-
- val STYLEABLE_ANIMATED_VECTOR_DRAWABLE = intArrayOf(android.R.attr.drawable)
- val STYLEABLE_ANIMATED_VECTOR_DRAWABLE_DRAWABLE = 0
- val STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET =
- intArrayOf(android.R.attr.name, android.R.attr.animation)
- val STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET_ANIMATION = 1
- val STYLEABLE_ANIMATED_VECTOR_DRAWABLE_TARGET_NAME = 0
-
- val STYLEABLE_ANIMATOR = intArrayOf(
- 0x01010141,
- 0x01010198,
- 0x010101be,
- 0x010101bf,
- 0x010101c0,
- 0x010102de,
- 0x010102df,
- 0x010102e0
- )
-
- val STYLEABLE_ANIMATOR_INTERPOLATOR = 0
- val STYLEABLE_ANIMATOR_DURATION = 1
- val STYLEABLE_ANIMATOR_START_OFFSET = 2
- val STYLEABLE_ANIMATOR_REPEAT_COUNT = 3
- val STYLEABLE_ANIMATOR_REPEAT_MODE = 4
- val STYLEABLE_ANIMATOR_VALUE_FROM = 5
- val STYLEABLE_ANIMATOR_VALUE_TO = 6
- val STYLEABLE_ANIMATOR_VALUE_TYPE = 7
- val STYLEABLE_ANIMATOR_SET = intArrayOf(0x010102e2)
- val STYLEABLE_ANIMATOR_SET_ORDERING = 0
-
- val STYLEABLE_PROPERTY_VALUES_HOLDER =
- intArrayOf(0x010102de, 0x010102df, 0x010102e0, 0x010102e1)
- val STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_FROM = 0
- val STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_TO = 1
- val STYLEABLE_PROPERTY_VALUES_HOLDER_VALUE_TYPE = 2
- val STYLEABLE_PROPERTY_VALUES_HOLDER_PROPERTY_NAME = 3
-
- val STYLEABLE_KEYFRAME = intArrayOf(0x01010024, 0x01010141, 0x010102e0, 0x010104d8)
- val STYLEABLE_KEYFRAME_VALUE = 0
- val STYLEABLE_KEYFRAME_INTERPOLATOR = 1
- val STYLEABLE_KEYFRAME_VALUE_TYPE = 2
- val STYLEABLE_KEYFRAME_FRACTION = 3
-
- val STYLEABLE_PROPERTY_ANIMATOR = intArrayOf(0x010102e1, 0x01010405, 0x01010474, 0x01010475)
- val STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_NAME = 0
- val STYLEABLE_PROPERTY_ANIMATOR_PATH_DATA = 1
- val STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_X_NAME = 2
- val STYLEABLE_PROPERTY_ANIMATOR_PROPERTY_Y_NAME = 3
-
- val STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR = intArrayOf(
- android.R.attr.tension,
- android.R.attr.extraTension
- )
- val STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR_TENSION = 0
- val STYLEABLE_ANTICIPATEOVERSHOOT_INTERPOLATOR_EXTRA_TENSION = 1
-
- val STYLEABLE_ACCELERATE_INTERPOLATOR = intArrayOf(android.R.attr.factor)
- val STYLEABLE_ACCELERATE_INTERPOLATOR_FACTOR = 0
-
- val STYLEABLE_DECELERATE_INTERPOLATOR = intArrayOf(android.R.attr.factor)
- val STYLEABLE_DECELERATE_INTERPOLATOR_FACTOR = 0
-
- val STYLEABLE_CYCLE_INTERPOLATOR = intArrayOf(android.R.attr.cycles)
- val STYLEABLE_CYCLE_INTERPOLATOR_CYCLES = 0
-
- val STYLEABLE_OVERSHOOT_INTERPOLATOR = intArrayOf(android.R.attr.tension)
- val STYLEABLE_OVERSHOOT_INTERPOLATOR_TENSION = 0
-
- val STYLEABLE_PATH_INTERPOLATOR =
- intArrayOf(0x010103fc, 0x010103fd, 0x010103fe, 0x010103ff, 0x01010405)
-
- val STYLEABLE_PATH_INTERPOLATOR_CONTROL_X_1 = 0
- val STYLEABLE_PATH_INTERPOLATOR_CONTROL_Y_1 = 1
-
- val STYLEABLE_PATH_INTERPOLATOR_CONTROL_X_2 = 2
- val STYLEABLE_PATH_INTERPOLATOR_CONTROL_Y_2 = 3
-
- val STYLEABLE_PATH_INTERPOLATOR_PATH_DATA = 4
-
- val FAST_OUT_LINEAR_IN = 0x010c000f
- val FAST_OUT_SLOW_IN = 0x010c000d
- val LINEAR_OUT_SLOW_IN = 0x010c000e
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index 748621d..ed7fb62 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -614,7 +614,10 @@
// We can't be confident that RenderNode is supported, so we try and fail over to
// the ViewLayer implementation. We'll try even on on P devices, but it will fail
// until ART allows things on the unsupported list on P.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && isRenderNodeCompatible) {
+ if (isHardwareAccelerated &&
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
+ isRenderNodeCompatible
+ ) {
try {
return RenderNodeLayer(
this,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
index c4b52b1..f320a6f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
@@ -26,6 +26,7 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCompositionContext
import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
@@ -101,6 +102,7 @@
*
* @param [image] ImageVector used to create a vector graphic sub-composition
*/
+@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun rememberVectorPainter(image: ImageVector) =
rememberVectorPainter(
@@ -215,137 +217,139 @@
}
/**
- * Returns all the properties of PathComponent or GroupComponent that can be overridden for
- * animation. This can be passed to [RenderVectorGroup] to override some property values when the
- * [VectorGroup] is rendered.
+ * Represents one of the properties for PathComponent or GroupComponent that can be overwritten
+ * when it is composed and drawn with [RenderVectorGroup].
*/
-internal interface VectorOverride {
-
- /**
- * Overrides the 'rotation' attribute for a vector group.
- */
- fun obtainRotation(rotation: Float): Float = rotation
-
- /**
- * Overrides the 'pivotX' attribute for a vector group.
- */
- fun obtainPivotX(pivotX: Float): Float = pivotX
-
- /**
- * Overrides the 'pivotY' attribute for a vector group.
- */
- fun obtainPivotY(pivotY: Float): Float = pivotY
-
- /**
- * Overrides the 'scaleX' attribute for a vector group.
- */
- fun obtainScaleX(scaleX: Float): Float = scaleX
-
- /**
- * Overrides the 'scaleY' attribute for a vector group.
- */
- fun obtainScaleY(scaleY: Float): Float = scaleY
-
- /**
- * Overrides the 'translateX' attribute for a vector group.
- */
- fun obtainTranslateX(translateX: Float): Float = translateX
-
- /**
- * Overrides the 'translateY' attribute for a vector group.
- */
- fun obtainTranslateY(translateY: Float): Float = translateY
-
- /**
- * Overrides the 'pathData' attribute for a vector path or a clip path.
- */
- fun obtainPathData(pathData: List<PathNode>): List<PathNode> = pathData
-
- /**
- * Overrides the 'fill' attribute for a vector path.
- */
- fun obtainFill(fill: Brush?): Brush? = fill
-
- /**
- * Overrides the 'fillAlpha' attribute for a vector path.
- */
- fun obtainFillAlpha(fillAlpha: Float): Float = fillAlpha
-
- /**
- * Overrides the 'stroke' attribute for a vector path.
- */
- fun obtainStroke(stroke: Brush?): Brush? = stroke
-
- /**
- * Overrides the 'strokeWidth' attribute for a vector path.
- */
- fun obtainStrokeWidth(strokeWidth: Float): Float = strokeWidth
-
- /**
- * Overrides the 'strokeAlpha' attribute for a vector path.
- */
- fun obtainStrokeAlpha(strokeAlpha: Float): Float = strokeAlpha
-
- /**
- * Overrides the 'trimPathStart' attribute for a vector path.
- */
- fun obtainTrimPathStart(trimPathStart: Float): Float = trimPathStart
-
- /**
- * Overrides the 'trimPathEnd' attribute for a vector path.
- */
- fun obtainTrimPathEnd(trimPathEnd: Float): Float = trimPathEnd
-
- /**
- * Overrides the 'trimPathOffset' attribute for a vector path.
- */
- fun obtainTrimPathOffset(trimPathOffset: Float): Float = trimPathOffset
+@ExperimentalComposeUiApi
+sealed class VectorProperty<T> {
+ object Rotation : VectorProperty<Float>()
+ object PivotX : VectorProperty<Float>()
+ object PivotY : VectorProperty<Float>()
+ object ScaleX : VectorProperty<Float>()
+ object ScaleY : VectorProperty<Float>()
+ object TranslateX : VectorProperty<Float>()
+ object TranslateY : VectorProperty<Float>()
+ object PathData : VectorProperty<List<PathNode>>()
+ object Fill : VectorProperty<Brush?>()
+ object FillAlpha : VectorProperty<Float>()
+ object Stroke : VectorProperty<Brush?>()
+ object StrokeLineWidth : VectorProperty<Float>()
+ object StrokeAlpha : VectorProperty<Float>()
+ object TrimPathStart : VectorProperty<Float>()
+ object TrimPathEnd : VectorProperty<Float>()
+ object TrimPathOffset : VectorProperty<Float>()
}
-private object DefaultVectorOverride : VectorOverride
+/**
+ * Holds a set of values that overwrite the original property values of an [ImageVector]. This
+ * allows you to dynamically change any of the property values provided as [VectorProperty].
+ * This can be passed to [RenderVectorGroup] to alter some property values when the [VectorGroup]
+ * is rendered.
+ */
+@ExperimentalComposeUiApi
+interface VectorConfig {
+ fun <T> getOrDefault(property: VectorProperty<T>, defaultValue: T): T {
+ return defaultValue
+ }
+}
/**
- * Recursive method for creating the vector graphic composition by traversing
- * the tree structure
+ * Recursively creates the vector graphic composition by traversing the tree structure.
+ *
+ * @param group The vector group to render.
+ * @param configs An optional map of [VectorConfig] to provide animation values. The keys are the
+ * node names. The values are [VectorConfig] for that node.
*/
+@ExperimentalComposeUiApi
@Composable
-internal fun RenderVectorGroup(
+fun RenderVectorGroup(
group: VectorGroup,
- overrides: Map<String, VectorOverride> = emptyMap()
+ configs: Map<String, VectorConfig> = emptyMap()
) {
for (vectorNode in group) {
if (vectorNode is VectorPath) {
- val override = overrides[vectorNode.name] ?: DefaultVectorOverride
+ val config = configs[vectorNode.name] ?: object : VectorConfig {}
Path(
- pathData = override.obtainPathData(vectorNode.pathData),
+ pathData = config.getOrDefault(
+ VectorProperty.PathData,
+ vectorNode.pathData
+ ),
pathFillType = vectorNode.pathFillType,
name = vectorNode.name,
- fill = override.obtainFill(vectorNode.fill),
- fillAlpha = override.obtainFillAlpha(vectorNode.fillAlpha),
- stroke = override.obtainStroke(vectorNode.stroke),
- strokeAlpha = override.obtainStrokeAlpha(vectorNode.strokeAlpha),
- strokeLineWidth = override.obtainStrokeWidth(vectorNode.strokeLineWidth),
+ fill = config.getOrDefault(
+ VectorProperty.Fill,
+ vectorNode.fill
+ ),
+ fillAlpha = config.getOrDefault(
+ VectorProperty.FillAlpha,
+ vectorNode.fillAlpha
+ ),
+ stroke = config.getOrDefault(
+ VectorProperty.Stroke,
+ vectorNode.stroke
+ ),
+ strokeAlpha = config.getOrDefault(
+ VectorProperty.StrokeAlpha,
+ vectorNode.strokeAlpha
+ ),
+ strokeLineWidth = config.getOrDefault(
+ VectorProperty.StrokeLineWidth,
+ vectorNode.strokeLineWidth
+ ),
strokeLineCap = vectorNode.strokeLineCap,
strokeLineJoin = vectorNode.strokeLineJoin,
strokeLineMiter = vectorNode.strokeLineMiter,
- trimPathStart = override.obtainTrimPathStart(vectorNode.trimPathStart),
- trimPathEnd = override.obtainTrimPathEnd(vectorNode.trimPathEnd),
- trimPathOffset = override.obtainTrimPathOffset(vectorNode.trimPathOffset)
+ trimPathStart = config.getOrDefault(
+ VectorProperty.TrimPathStart,
+ vectorNode.trimPathStart
+ ),
+ trimPathEnd = config.getOrDefault(
+ VectorProperty.TrimPathEnd,
+ vectorNode.trimPathEnd
+ ),
+ trimPathOffset = config.getOrDefault(
+ VectorProperty.TrimPathOffset,
+ vectorNode.trimPathOffset
+ )
)
} else if (vectorNode is VectorGroup) {
- val override = overrides[vectorNode.name] ?: DefaultVectorOverride
+ val config = configs[vectorNode.name] ?: object : VectorConfig {}
Group(
name = vectorNode.name,
- rotation = override.obtainRotation(vectorNode.rotation),
- scaleX = override.obtainScaleX(vectorNode.scaleX),
- scaleY = override.obtainScaleY(vectorNode.scaleY),
- translationX = override.obtainTranslateX(vectorNode.translationX),
- translationY = override.obtainTranslateY(vectorNode.translationY),
- pivotX = override.obtainPivotX(vectorNode.pivotX),
- pivotY = override.obtainPivotY(vectorNode.pivotY),
- clipPathData = override.obtainPathData(vectorNode.clipPathData)
+ rotation = config.getOrDefault(
+ VectorProperty.Rotation,
+ vectorNode.rotation
+ ),
+ scaleX = config.getOrDefault(
+ VectorProperty.ScaleX,
+ vectorNode.scaleX
+ ),
+ scaleY = config.getOrDefault(
+ VectorProperty.ScaleY,
+ vectorNode.scaleY
+ ),
+ translationX = config.getOrDefault(
+ VectorProperty.TranslateX,
+ vectorNode.translationX
+ ),
+ translationY = config.getOrDefault(
+ VectorProperty.TranslateY,
+ vectorNode.translationY
+ ),
+ pivotX = config.getOrDefault(
+ VectorProperty.PivotX,
+ vectorNode.pivotX
+ ),
+ pivotY = config.getOrDefault(
+ VectorProperty.PivotY,
+ vectorNode.pivotY
+ ),
+ clipPathData = config.getOrDefault(
+ VectorProperty.PathData,
+ vectorNode.clipPathData
+ )
) {
- RenderVectorGroup(group = vectorNode, overrides = overrides)
+ RenderVectorGroup(group = vectorNode, configs = configs)
}
}
}
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..3b15333 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?);
@@ -1485,10 +1488,23 @@
package androidx.core.math {
public class MathUtils {
+ method public static int addExact(int, int);
+ method public static long addExact(long, long);
method public static float clamp(float, float, float);
method public static double clamp(double, double, double);
method public static int clamp(int, int, int);
method public static long clamp(long, long, long);
+ method public static int decrementExact(int);
+ method public static long decrementExact(long);
+ method public static int incrementExact(int);
+ method public static long incrementExact(long);
+ method public static int multiplyExact(int, int);
+ method public static long multiplyExact(long, long);
+ method public static int negateExact(int);
+ method public static long negateExact(long);
+ method public static int subtractExact(int, int);
+ method public static long subtractExact(long, long);
+ method public static int toIntExact(long);
}
}
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index 334f7dd..807201b 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?);
@@ -1485,10 +1488,23 @@
package androidx.core.math {
public class MathUtils {
+ method public static int addExact(int, int);
+ method public static long addExact(long, long);
method public static float clamp(float, float, float);
method public static double clamp(double, double, double);
method public static int clamp(int, int, int);
method public static long clamp(long, long, long);
+ method public static int decrementExact(int);
+ method public static long decrementExact(long);
+ method public static int incrementExact(int);
+ method public static long incrementExact(long);
+ method public static int multiplyExact(int, int);
+ method public static long multiplyExact(long, long);
+ method public static int negateExact(int);
+ method public static long negateExact(long);
+ method public static int subtractExact(int, int);
+ method public static long subtractExact(long, long);
+ method public static int toIntExact(long);
}
}
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 61cafa1..86d4e47 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?);
@@ -1806,10 +1809,23 @@
package androidx.core.math {
public class MathUtils {
+ method public static int addExact(int, int);
+ method public static long addExact(long, long);
method public static float clamp(float, float, float);
method public static double clamp(double, double, double);
method public static int clamp(int, int, int);
method public static long clamp(long, long, long);
+ method public static int decrementExact(int);
+ method public static long decrementExact(long);
+ method public static int incrementExact(int);
+ method public static long incrementExact(long);
+ method public static int multiplyExact(int, int);
+ method public static long multiplyExact(long, long);
+ method public static int negateExact(int);
+ method public static long negateExact(long);
+ method public static int subtractExact(int, int);
+ method public static long subtractExact(long, long);
+ method public static int toIntExact(long);
}
}
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/androidTest/java/androidx/core/math/MathUtilsTest.java b/core/core/src/androidTest/java/androidx/core/math/MathUtilsTest.java
index a717d3d..856acd4 100644
--- a/core/core/src/androidTest/java/androidx/core/math/MathUtilsTest.java
+++ b/core/core/src/androidTest/java/androidx/core/math/MathUtilsTest.java
@@ -17,6 +17,7 @@
package androidx.core.math;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -29,6 +30,78 @@
public class MathUtilsTest {
@Test
+ public void testAddExact() {
+ assertEquals(2, MathUtils.addExact(1, 1));
+ assertEquals(2L, MathUtils.addExact(1L, 1L));
+ assertEquals(0, MathUtils.addExact(1, -1));
+ assertEquals(0L, MathUtils.addExact(1L, -1L));
+ assertThrows(ArithmeticException.class, () -> MathUtils.addExact(Integer.MAX_VALUE, 1));
+ assertThrows(ArithmeticException.class, () -> MathUtils.addExact(Long.MAX_VALUE, 1L));
+ assertThrows(ArithmeticException.class, () -> MathUtils.addExact(Integer.MIN_VALUE, -1));
+ assertThrows(ArithmeticException.class, () -> MathUtils.addExact(Long.MIN_VALUE, -1L));
+ }
+
+ @Test
+ public void testSubtractExact() {
+ assertEquals(0, MathUtils.subtractExact(1, 1));
+ assertEquals(0L, MathUtils.subtractExact(1L, 1L));
+ assertEquals(2, MathUtils.subtractExact(1, -1));
+ assertEquals(2L, MathUtils.subtractExact(1L, -1L));
+ assertThrows(ArithmeticException.class,
+ () -> MathUtils.subtractExact(Integer.MAX_VALUE, -1));
+ assertThrows(ArithmeticException.class, () -> MathUtils.subtractExact(Long.MAX_VALUE, -1L));
+ assertThrows(ArithmeticException.class,
+ () -> MathUtils.subtractExact(Integer.MIN_VALUE, 1));
+ assertThrows(ArithmeticException.class, () -> MathUtils.subtractExact(Long.MIN_VALUE, 1L));
+ }
+
+ @Test
+ public void testMultiplyExact() {
+ assertEquals(4, MathUtils.multiplyExact(2, 2));
+ assertEquals(4L, MathUtils.multiplyExact(2L, 2L));
+ assertEquals(0, MathUtils.multiplyExact(2, 0));
+ assertEquals(0L, MathUtils.multiplyExact(2L, 0L));
+ assertEquals(-4, MathUtils.multiplyExact(2, -2));
+ assertEquals(-4L, MathUtils.multiplyExact(2L, -2L));
+ assertThrows(ArithmeticException.class,
+ () -> MathUtils.multiplyExact(Integer.MAX_VALUE, 2));
+ assertThrows(ArithmeticException.class, () -> MathUtils.multiplyExact(Long.MAX_VALUE, 2L));
+ assertThrows(ArithmeticException.class,
+ () -> MathUtils.multiplyExact(Integer.MIN_VALUE, 2));
+ assertThrows(ArithmeticException.class, () -> MathUtils.multiplyExact(Long.MIN_VALUE, 2L));
+ }
+
+ @Test
+ public void testIncrementExact() {
+ assertEquals(1, MathUtils.incrementExact(0));
+ assertEquals(1L, MathUtils.incrementExact(0L));
+ assertThrows(ArithmeticException.class, () -> MathUtils.incrementExact(Integer.MAX_VALUE));
+ assertThrows(ArithmeticException.class, () -> MathUtils.incrementExact(Long.MAX_VALUE));
+ }
+
+ @Test
+ public void testDecrementExact() {
+ assertEquals(-1, MathUtils.decrementExact(0));
+ assertEquals(-1L, MathUtils.decrementExact(0L));
+ assertThrows(ArithmeticException.class, () -> MathUtils.decrementExact(Integer.MIN_VALUE));
+ assertThrows(ArithmeticException.class, () -> MathUtils.decrementExact(Long.MIN_VALUE));
+ }
+
+ @Test
+ public void testNegateExact() {
+ assertEquals(Integer.MIN_VALUE + 1, MathUtils.negateExact(Integer.MAX_VALUE));
+ assertEquals(Long.MIN_VALUE + 1, MathUtils.negateExact(Long.MAX_VALUE));
+ assertThrows(ArithmeticException.class, () -> MathUtils.negateExact(Integer.MIN_VALUE));
+ assertThrows(ArithmeticException.class, () -> MathUtils.negateExact(Long.MIN_VALUE));
+ }
+
+ @Test
+ public void testToIntExact() {
+ assertEquals(1, MathUtils.toIntExact(1L));
+ assertThrows(ArithmeticException.class, () -> MathUtils.toIntExact(Long.MAX_VALUE));
+ }
+
+ @Test
public void testClamp() {
// Int
assertEquals(0, MathUtils.clamp(-4, 0, 7));
diff --git a/core/core/src/main/java/androidx/core/app/NotificationCompat.java b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
index ff63363..25389a0 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
@@ -5155,15 +5155,15 @@
* To create a notification with wearable extensions:
* <ol>
* <li>Create a {@link NotificationCompat.Builder}, setting any desired
- * properties.
- * <li>Create a {@link NotificationCompat.WearableExtender}.
+ * properties.</li>
+ * <li>Create a {@link NotificationCompat.WearableExtender}.</li>
* <li>Set wearable-specific properties using the
- * {@code add} and {@code set} methods of {@link NotificationCompat.WearableExtender}.
+ * {@code add} and {@code set} methods of {@link NotificationCompat.WearableExtender}.</li>
* <li>Call {@link NotificationCompat.Builder#extend} to apply the extensions to a
- * notification.
+ * notification.</li>
* <li>Post the notification to the notification
* system with the {@code NotificationManagerCompat.notify(...)} methods
- * and not the {@code NotificationManager.notify(...)} methods.
+ * and not the {@code NotificationManager.notify(...)} methods.</li>
* </ol>
*
* <pre class="prettyprint">
@@ -6122,15 +6122,15 @@
*
* <ol>
* <li>Create an {@link NotificationCompat.Builder}, setting any desired
- * properties.
- * <li>Create a {@link CarExtender}.
+ * properties.</li>
+ * <li>Create a {@link CarExtender}.</li>
* <li>Set car-specific properties using the {@code add} and {@code set} methods of
- * {@link CarExtender}.
+ * {@link CarExtender}.</li>
* <li>Call {@link androidx.core.app.NotificationCompat.Builder#extend(NotificationCompat.Extender)}
- * to apply the extensions to a notification.
+ * to apply the extensions to a notification.</li>
* <li>Post the notification to the notification system with the
* {@code NotificationManagerCompat.notify(...)} methods and not the
- * {@code NotificationManager.notify(...)} methods.
+ * {@code NotificationManager.notify(...)} methods.</li>
* </ol>
*
* <pre class="prettyprint">
diff --git a/core/core/src/main/java/androidx/core/content/FileProvider.java b/core/core/src/main/java/androidx/core/content/FileProvider.java
index d882a0a..b8e5c80 100644
--- a/core/core/src/main/java/androidx/core/content/FileProvider.java
+++ b/core/core/src/main/java/androidx/core/content/FileProvider.java
@@ -76,7 +76,7 @@
* <ol>
* <li><a href="#ProviderDefinition">Defining a FileProvider</a></li>
* <li><a href="#SpecifyFiles">Specifying Available Files</a></li>
- * <li><a href="#GetUri">Retrieving the Content URI for a File</li>
+ * <li><a href="#GetUri">Retrieving the Content URI for a File</a></li>
* <li><a href="#Permissions">Granting Temporary Permissions to a URI</a></li>
* <li><a href="#ServeUri">Serving a Content URI to Another App</a></li>
* </ol>
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/core/core/src/main/java/androidx/core/math/MathUtils.java b/core/core/src/main/java/androidx/core/math/MathUtils.java
index e6158bc..8a42777 100644
--- a/core/core/src/main/java/androidx/core/math/MathUtils.java
+++ b/core/core/src/main/java/androidx/core/math/MathUtils.java
@@ -24,6 +24,175 @@
private MathUtils() {}
/**
+ * See {@link Math#addExact(int, int)}.
+ */
+ public static int addExact(int x, int y) {
+ // copied from Math.java
+ int r = x + y;
+ // HD 2-12 Overflow iff both arguments have the opposite sign of the result
+ if (((x ^ r) & (y ^ r)) < 0) {
+ throw new ArithmeticException("integer overflow");
+ }
+ return r;
+ }
+
+ /**
+ * See {@link Math#addExact(long, long)}.
+ */
+ public static long addExact(long x, long y) {
+ // copied from Math.java
+ long r = x + y;
+ // HD 2-12 Overflow iff both arguments have the opposite sign of the result
+ if (((x ^ r) & (y ^ r)) < 0) {
+ throw new ArithmeticException("long overflow");
+ }
+ return r;
+ }
+
+
+ /**
+ * See {@link Math#subtractExact(int, int)}.
+ */
+ public static int subtractExact(int x, int y) {
+ // copied from Math.java
+ int r = x - y;
+ // HD 2-12 Overflow iff the arguments have different signs and
+ // the sign of the result is different than the sign of x
+ if (((x ^ y) & (x ^ r)) < 0) {
+ throw new ArithmeticException("integer overflow");
+ }
+ return r;
+ }
+
+ /**
+ * See {@link Math#subtractExact(long, long)}.
+ */
+ public static long subtractExact(long x, long y) {
+ // copied from Math.java
+ long r = x - y;
+ // HD 2-12 Overflow iff the arguments have different signs and
+ // the sign of the result is different than the sign of x
+ if (((x ^ y) & (x ^ r)) < 0) {
+ throw new ArithmeticException("long overflow");
+ }
+ return r;
+ }
+
+ /**
+ * See {@link Math#multiplyExact(int, int)}.
+ */
+ public static int multiplyExact(int x, int y) {
+ // copied from Math.java
+ long r = (long) x * (long) y;
+ if ((int) r != r) {
+ throw new ArithmeticException("integer overflow");
+ }
+ return (int) r;
+ }
+
+ /**
+ * See {@link Math#multiplyExact(long, long)}.
+ */
+ public static long multiplyExact(long x, long y) {
+ // copied from Math.java
+ long r = x * y;
+ long ax = Math.abs(x);
+ long ay = Math.abs(y);
+ if (((ax | ay) >>> 31 != 0)) {
+ // Some bits greater than 2^31 that might cause overflow
+ // Check the result using the divide operator
+ // and check for the special case of Long.MIN_VALUE * -1
+ if (((y != 0) && (r / y != x)) || (x == Long.MIN_VALUE && y == -1)) {
+ throw new ArithmeticException("long overflow");
+ }
+ }
+ return r;
+ }
+
+ /**
+ * See {@link Math#incrementExact(int)}.
+ */
+ public static int incrementExact(int a) {
+ // copied from Math.java
+ if (a == Integer.MAX_VALUE) {
+ throw new ArithmeticException("integer overflow");
+ }
+
+ return a + 1;
+ }
+
+ /**
+ * See {@link Math#incrementExact(long)}.
+ */
+ public static long incrementExact(long a) {
+ // copied from Math.java
+ if (a == Long.MAX_VALUE) {
+ throw new ArithmeticException("long overflow");
+ }
+
+ return a + 1L;
+ }
+
+ /**
+ * See {@link Math#decrementExact(int)}.
+ */
+ public static int decrementExact(int a) {
+ // copied from Math.java
+ if (a == Integer.MIN_VALUE) {
+ throw new ArithmeticException("integer overflow");
+ }
+
+ return a - 1;
+ }
+
+ /**
+ * See {@link Math#decrementExact(long)}.
+ */
+ public static long decrementExact(long a) {
+ // copied from Math.java
+ if (a == Long.MIN_VALUE) {
+ throw new ArithmeticException("long overflow");
+ }
+
+ return a - 1L;
+ }
+
+ /**
+ * See {@link Math#negateExact(int)}.
+ */
+ public static int negateExact(int a) {
+ // copied from Math.java
+ if (a == Integer.MIN_VALUE) {
+ throw new ArithmeticException("integer overflow");
+ }
+
+ return -a;
+ }
+
+ /**
+ * See {@link Math#negateExact(long)}.
+ */
+ public static long negateExact(long a) {
+ // copied from Math.java
+ if (a == Long.MIN_VALUE) {
+ throw new ArithmeticException("long overflow");
+ }
+
+ return -a;
+ }
+
+ /**
+ * See {@link Math#toIntExact(long)}.
+ */
+ public static int toIntExact(long value) {
+ // copied from Math.java
+ if ((int) value != value) {
+ throw new ArithmeticException("integer overflow");
+ }
+ return (int) value;
+ }
+
+ /**
* This method takes a numerical value and ensures it fits in a given numerical range. If the
* number is smaller than the minimum required by the range, then the minimum of the range will
* be returned. If the number is higher than the maximum allowed by the range then the maximum
diff --git a/core/core/src/main/java/androidx/core/view/ViewCompat.java b/core/core/src/main/java/androidx/core/view/ViewCompat.java
index 0c626e0..4fb240c 100644
--- a/core/core/src/main/java/androidx/core/view/ViewCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ViewCompat.java
@@ -796,14 +796,14 @@
*
* <ol>
* <li>When the view contents is irrelevant for autofill (for example, a text field used in a
- * "Captcha" challenge), it should be {@link View#IMPORTANT_FOR_AUTOFILL_NO}.
+ * "Captcha" challenge), it should be {@link View#IMPORTANT_FOR_AUTOFILL_NO}.</li>
* <li>When both the view and its children are irrelevant for autofill (for example, the root
* view of an activity containing a spreadhseet editor), it should be
- * {@link View#IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS}.
+ * {@link View#IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS}.</li>
* <li>When the view content is relevant for autofill but its children aren't (for example,
* a credit card expiration date represented by a custom view that overrides the proper
* autofill methods and has 2 children representing the month and year), it should
- * be {@link View#IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS}.
+ * be {@link View#IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS}.</li>
* </ol>
*
* <p><strong>NOTE:</strong> setting the mode as does {@link View#IMPORTANT_FOR_AUTOFILL_NO} or
@@ -837,9 +837,9 @@
*
* <p>Generally speaking, a view is important for autofill if:
* <ol>
- * <li>The view can be autofilled by an {@link android.service.autofill.AutofillService}.
+ * <li>The view can be autofilled by an {@link android.service.autofill.AutofillService}.</li>
* <li>The view contents can help an {@link android.service.autofill.AutofillService}
- * determine how other views can be autofilled.
+ * determine how other views can be autofilled.</li>
* </ol>
*
* <p>For example, view containers should typically return {@code false} for performance reasons
@@ -855,14 +855,14 @@
* <ol>
* <li>if it returns {@link View#IMPORTANT_FOR_AUTOFILL_YES} or
* {@link View#IMPORTANT_FOR_AUTOFILL_YES_EXCLUDE_DESCENDANTS},
- * then it returns {@code true}
+ * then it returns {@code true}</li>
* <li>if it returns {@link View#IMPORTANT_FOR_AUTOFILL_NO} or
* {@link View#IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS},
- * then it returns {@code false}
+ * then it returns {@code false}</li>
* <li>if it returns {@link View#IMPORTANT_FOR_AUTOFILL_AUTO},
* then it uses some simple heuristics that can return {@code true}
- * in some cases (like a container with a resource id), but {@code false} in most.
- * <li>otherwise, it returns {@code false}.
+ * in some cases (like a container with a resource id), but {@code false} in most.</li>
+ * <li>otherwise, it returns {@code false}.</li>
* </ol>
*
* <p>When a view is considered important for autofill:
@@ -1156,19 +1156,19 @@
* <ol>
* <li>{@link #IMPORTANT_FOR_ACCESSIBILITY_NO} or
* {@link #IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS}, return <code>false
- * </code>
- * <li>{@link #IMPORTANT_FOR_ACCESSIBILITY_YES}, return <code>true</code>
+ * </code></li>
+ * <li>{@link #IMPORTANT_FOR_ACCESSIBILITY_YES}, return <code>true</code></li>
* <li>{@link #IMPORTANT_FOR_ACCESSIBILITY_AUTO}, return <code>true</code> if
- * view satisfies any of the following:
+ * view satisfies any of the following:</li>
* <ul>
* <li>Is actionable, e.g. {@link View#isClickable()},
- * {@link View#isLongClickable()}, or {@link View#isFocusable()}
- * <li>Has an {@link AccessibilityDelegateCompat}
+ * {@link View#isLongClickable()}, or {@link View#isFocusable()}</li>
+ * <li>Has an {@link AccessibilityDelegateCompat}</li>
* <li>Has an interaction listener, e.g. {@link View.OnTouchListener},
- * {@link View.OnKeyListener}, etc.
+ * {@link View.OnKeyListener}, etc.</li>
* <li>Is an accessibility live region, e.g.
* {@link #getAccessibilityLiveRegion(View)} is not
- * {@link #ACCESSIBILITY_LIVE_REGION_NONE}.
+ * {@link #ACCESSIBILITY_LIVE_REGION_NONE}.</li>
* </ul>
* </ol>
* <p>
@@ -2720,9 +2720,9 @@
* scenarios:
* <ol>
* <li>Paste from the clipboard (e.g. "Paste" or "Paste as plain text" action in the
- * insertion/selection menu)
- * <li>Content insertion from the keyboard (from {@link InputConnection#commitContent})
- * <li>Drag and drop (drop events from {@link View#onDragEvent})
+ * insertion/selection menu)</li>
+ * <li>Content insertion from the keyboard (from {@link InputConnection#commitContent})</li>
+ * <li>Drag and drop (drop events from {@link View#onDragEvent})</li>
* </ol>
*
* <p>When setting a listener, clients must also declare the accepted MIME types.
diff --git a/core/core/src/main/java/androidx/core/widget/NestedScrollView.java b/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
index 7f6835f..ab9cc52 100644
--- a/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
+++ b/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
@@ -362,8 +362,8 @@
@Override
public boolean onStartNestedScroll(
- @NonNull View child, @NonNull View target, int nestedScrollAxes) {
- return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
+ @NonNull View child, @NonNull View target, int axes) {
+ return onStartNestedScroll(child, target, axes, ViewCompat.TYPE_TOUCH);
}
@Override
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/file-utils/diff-filterer.py b/development/file-utils/diff-filterer.py
index 3bc3ccf..168cc9b 100755
--- a/development/file-utils/diff-filterer.py
+++ b/development/file-utils/diff-filterer.py
@@ -16,7 +16,7 @@
#
-import datetime, filecmp, math, multiprocessing, os, psutil, shutil, subprocess, stat, sys, time
+import datetime, filecmp, math, multiprocessing, os, shutil, subprocess, stat, sys, time
from collections import OrderedDict
def usage():
@@ -128,6 +128,16 @@
fileIo = FileIo()
+# Returns cpu usage
+class CpuStats(object):
+
+ def cpu_times_percent(self):
+ # We wait to attempt to import psutil in case we don't need it and it doesn't exist on this system
+ import psutil
+ return psutil.cpu_times_percent(interval=None)
+
+cpuStats = CpuStats()
+
# Fast file copying
class FileCopyCache(object):
def __init__(self):
@@ -844,7 +854,7 @@
else:
# If N jobs are running then wait for all N to fail before increasing the number of running jobs
# Recalibrate the number of processes based on the system load
- systemUsageStats = psutil.cpu_times_percent(interval=None)
+ systemUsageStats = cpuStats.cpu_times_percent()
systemIdleFraction = systemUsageStats.idle / 100
if systemIdleFraction >= 0.5:
if numCompletionsSinceLastPoolSizeChange <= len(activeTestStatesById):
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/development/update_studio.sh b/development/update_studio.sh
index d2581e4..1256678 100755
--- a/development/update_studio.sh
+++ b/development/update_studio.sh
@@ -1,7 +1,7 @@
#!/bin/bash
# Get versions
-AGP_VERSION=${1:-7.1.0-alpha01}
-STUDIO_VERSION_STRING=${2:-"Android Studio Bumblebee (2021.1.1) Canary 1"}
+AGP_VERSION=${1:-7.1.0-alpha03}
+STUDIO_VERSION_STRING=${2:-"Android Studio Bumblebee (2021.1.1) Canary 3"}
STUDIO_IFRAME_LINK=`curl "https://developer.android.com/studio/archive.html" | grep iframe | sed "s/.*src=\"\([a-zA-Z0-9\/\._]*\)\".*/https:\/\/android-dot-devsite-v2-prod.appspot.com\1/g"`
STUDIO_LINK=`curl -s $STUDIO_IFRAME_LINK | grep -C30 "$STUDIO_VERSION_STRING" | grep Linux | tail -n 1 | sed 's/.*a href="\(.*\).*"/\1/g'`
STUDIO_VERSION=`echo $STUDIO_LINK | sed "s/.*ide-zips\/\(.*\)\/android-studio-.*/\1/g"`
@@ -22,6 +22,6 @@
./development/importMaven/import_maven_artifacts.py -n com.android.tools.lint:lint-gradle:$LINT_VERSION
# Update studio_versions.properties
-sed -i "s/androidGradlePlugin = .*/androidGradlePlugin = $AGP_VERSION/g" gradle/libs.versions.toml
-sed -i "s/androidLint = .*/androidLint = $LINT_VERSION/g" gradle/libs.versions.toml
-sed -i "s/androidStudio = .*/androidStudio = $STUDIO_VERSION/g" gradle/libs.versions.toml
+sed -i "s/androidGradlePlugin = .*/androidGradlePlugin = \"$AGP_VERSION\"/g" gradle/libs.versions.toml
+sed -i "s/androidLint = .*/androidLint = \"$LINT_VERSION\"/g" gradle/libs.versions.toml
+sed -i "s/androidStudio = .*/androidStudio = \"$STUDIO_VERSION\"/g" gradle/libs.versions.toml
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 587c7cc..18ecca9 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -4,10 +4,10 @@
}
dependencies {
- docs("androidx.activity:activity:1.3.0-rc01")
- docs("androidx.activity:activity-compose:1.3.0-rc01")
- samples("androidx.activity:activity-compose-samples:1.3.0-rc01")
- docs("androidx.activity:activity-ktx:1.3.0-rc01")
+ docs("androidx.activity:activity:1.3.0-rc02")
+ docs("androidx.activity:activity-compose:1.3.0-rc02")
+ samples("androidx.activity:activity-compose-samples:1.3.0-rc02")
+ docs("androidx.activity:activity-ktx:1.3.0-rc02")
docs("androidx.ads:ads-identifier:1.0.0-alpha04")
docs("androidx.ads:ads-identifier-provider:1.0.0-alpha04")
docs("androidx.annotation:annotation:1.3.0-alpha01")
@@ -41,46 +41,46 @@
docs("androidx.cardview:cardview:1.0.0")
docs("androidx.collection:collection:1.2.0-alpha01")
docs("androidx.collection:collection-ktx:1.2.0-alpha01")
- docs("androidx.compose.animation:animation:1.0.0-rc01")
- docs("androidx.compose.animation:animation-core:1.0.0-rc01")
- samples("androidx.compose.animation:animation-samples:1.0.0-rc01")
- samples("androidx.compose.animation:animation-core-samples:1.0.0-rc01")
- docs("androidx.compose.foundation:foundation:1.0.0-rc01")
- docs("androidx.compose.foundation:foundation-layout:1.0.0-rc01")
- samples("androidx.compose.foundation:foundation-layout-samples:1.0.0-rc01")
- samples("androidx.compose.foundation:foundation-samples:1.0.0-rc01")
- docs("androidx.compose.material:material:1.0.0-rc01")
- docs("androidx.compose.material:material-icons-core:1.0.0-rc01")
- samples("androidx.compose.material:material-icons-core-samples:1.0.0-rc01")
- docs("androidx.compose.material:material-ripple:1.0.0-rc01")
- samples("androidx.compose.material:material-samples:1.0.0-rc01")
- docs("androidx.compose.runtime:runtime:1.0.0-rc01")
- docs("androidx.compose.runtime:runtime-livedata:1.0.0-rc01")
- samples("androidx.compose.runtime:runtime-livedata-samples:1.0.0-rc01")
- docs("androidx.compose.runtime:runtime-rxjava2:1.0.0-rc01")
- samples("androidx.compose.runtime:runtime-rxjava2-samples:1.0.0-rc01")
- docs("androidx.compose.runtime:runtime-rxjava3:1.0.0-rc01")
- samples("androidx.compose.runtime:runtime-rxjava3-samples:1.0.0-rc01")
- docs("androidx.compose.runtime:runtime-saveable:1.0.0-rc01")
- samples("androidx.compose.runtime:runtime-saveable-samples:1.0.0-rc01")
- samples("androidx.compose.runtime:runtime-samples:1.0.0-rc01")
- docs("androidx.compose.ui:ui:1.0.0-rc01")
- docs("androidx.compose.ui:ui-geometry:1.0.0-rc01")
- docs("androidx.compose.ui:ui-graphics:1.0.0-rc01")
- samples("androidx.compose.ui:ui-graphics-samples:1.0.0-rc01")
- docs("androidx.compose.ui:ui-test:1.0.0-rc01")
- docs("androidx.compose.ui:ui-test-junit4:1.0.0-rc01")
- docs("androidx.compose.ui:ui-text:1.0.0-rc01")
- samples("androidx.compose.ui:ui-text-samples:1.0.0-rc01")
- docs("androidx.compose.ui:ui-tooling:1.0.0-rc01")
- docs("androidx.compose.ui:ui-tooling-data:1.0.0-rc01")
- docs("androidx.compose.ui:ui-tooling-preview:1.0.0-rc01")
- docs("androidx.compose.ui:ui-unit:1.0.0-rc01")
- samples("androidx.compose.ui:ui-unit-samples:1.0.0-rc01")
- docs("androidx.compose.ui:ui-util:1.0.0-rc01")
- docs("androidx.compose.ui:ui-viewbinding:1.0.0-rc01")
- samples("androidx.compose.ui:ui-viewbinding-samples:1.0.0-rc01")
- samples("androidx.compose.ui:ui-samples:1.0.0-rc01")
+ docs("androidx.compose.animation:animation:1.0.0-rc02")
+ docs("androidx.compose.animation:animation-core:1.0.0-rc02")
+ samples("androidx.compose.animation:animation-samples:1.0.0-rc02")
+ samples("androidx.compose.animation:animation-core-samples:1.0.0-rc02")
+ docs("androidx.compose.foundation:foundation:1.0.0-rc02")
+ docs("androidx.compose.foundation:foundation-layout:1.0.0-rc02")
+ samples("androidx.compose.foundation:foundation-layout-samples:1.0.0-rc02")
+ samples("androidx.compose.foundation:foundation-samples:1.0.0-rc02")
+ docs("androidx.compose.material:material:1.0.0-rc02")
+ docs("androidx.compose.material:material-icons-core:1.0.0-rc02")
+ samples("androidx.compose.material:material-icons-core-samples:1.0.0-rc02")
+ docs("androidx.compose.material:material-ripple:1.0.0-rc02")
+ samples("androidx.compose.material:material-samples:1.0.0-rc02")
+ docs("androidx.compose.runtime:runtime:1.0.0-rc02")
+ docs("androidx.compose.runtime:runtime-livedata:1.0.0-rc02")
+ samples("androidx.compose.runtime:runtime-livedata-samples:1.0.0-rc02")
+ docs("androidx.compose.runtime:runtime-rxjava2:1.0.0-rc02")
+ samples("androidx.compose.runtime:runtime-rxjava2-samples:1.0.0-rc02")
+ docs("androidx.compose.runtime:runtime-rxjava3:1.0.0-rc02")
+ samples("androidx.compose.runtime:runtime-rxjava3-samples:1.0.0-rc02")
+ docs("androidx.compose.runtime:runtime-saveable:1.0.0-rc02")
+ samples("androidx.compose.runtime:runtime-saveable-samples:1.0.0-rc02")
+ samples("androidx.compose.runtime:runtime-samples:1.0.0-rc02")
+ docs("androidx.compose.ui:ui:1.0.0-rc02")
+ docs("androidx.compose.ui:ui-geometry:1.0.0-rc02")
+ docs("androidx.compose.ui:ui-graphics:1.0.0-rc02")
+ samples("androidx.compose.ui:ui-graphics-samples:1.0.0-rc02")
+ docs("androidx.compose.ui:ui-test:1.0.0-rc02")
+ docs("androidx.compose.ui:ui-test-junit4:1.0.0-rc02")
+ docs("androidx.compose.ui:ui-text:1.0.0-rc02")
+ samples("androidx.compose.ui:ui-text-samples:1.0.0-rc02")
+ docs("androidx.compose.ui:ui-tooling:1.0.0-rc02")
+ docs("androidx.compose.ui:ui-tooling-data:1.0.0-rc02")
+ docs("androidx.compose.ui:ui-tooling-preview:1.0.0-rc02")
+ docs("androidx.compose.ui:ui-unit:1.0.0-rc02")
+ samples("androidx.compose.ui:ui-unit-samples:1.0.0-rc02")
+ docs("androidx.compose.ui:ui-util:1.0.0-rc02")
+ docs("androidx.compose.ui:ui-viewbinding:1.0.0-rc02")
+ samples("androidx.compose.ui:ui-viewbinding-samples:1.0.0-rc02")
+ samples("androidx.compose.ui:ui-samples:1.0.0-rc02")
docs("androidx.concurrent:concurrent-futures:1.1.0")
docs("androidx.concurrent:concurrent-futures-ktx:1.1.0")
docs("androidx.contentpager:contentpager:1.0.0")
@@ -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")
@@ -189,7 +190,7 @@
docs("androidx.preference:preference:1.1.1")
docs("androidx.preference:preference-ktx:1.1.1")
docs("androidx.print:print:1.1.0-beta01")
- docs("androidx.profileinstaller:profileinstaller:1.0.0-rc01")
+ docs("androidx.profileinstaller:profileinstaller:1.0.0-rc02")
docs("androidx.recommendation:recommendation:1.0.0")
docs("androidx.recyclerview:recyclerview:1.2.1")
docs("androidx.recyclerview:recyclerview-selection:2.0.0-alpha01")
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index bffcfaa..1f398f4 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -134,6 +134,7 @@
docs(project(":hilt:hilt-common"))
docs(project(":hilt:hilt-navigation"))
docs(project(":hilt:hilt-navigation-compose"))
+ samples(project(":hilt:hilt-navigation-compose-samples"))
docs(project(":hilt:hilt-navigation-fragment"))
docs(project(":hilt:hilt-work"))
docs(project(":interpolator:interpolator"))
diff --git a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
index b2ebafd..0c15f69 100644
--- a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
+++ b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
@@ -428,6 +428,7 @@
@LargeTest
public void testDngFiles() throws Throwable {
readFromFilesWithExif(DNG_WITH_EXIF_WITH_XMP, R.array.dng_with_exif_with_xmp);
+ writeToFilesWithExif(DNG_WITH_EXIF_WITH_XMP, R.array.dng_with_exif_with_xmp);
}
@Test
diff --git a/exifinterface/exifinterface/src/androidTest/res/values/arrays.xml b/exifinterface/exifinterface/src/androidTest/res/values/arrays.xml
index f2bd02f..bc0947f 100644
--- a/exifinterface/exifinterface/src/androidTest/res/values/arrays.xml
+++ b/exifinterface/exifinterface/src/androidTest/res/values/arrays.xml
@@ -229,7 +229,7 @@
<item>337</item>
<item>600</item>
<item>800</item>
- <item>1</item>
+ <item>0</item>
<item>0</item>
<item>true</item>
<item>826</item>
diff --git a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
index 0fd8eef..a5e2e62 100644
--- a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
+++ b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
@@ -85,7 +85,7 @@
* <p>
* Supported for reading: JPEG, PNG, WebP, HEIF, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW, RAF.
* <p>
- * Supported for writing: JPEG, PNG, WebP.
+ * Supported for writing: JPEG, PNG, WebP, DNG.
*/
public class ExifInterface {
private static final String TAG = "ExifInterface";
@@ -3722,6 +3722,7 @@
new ExifTag(TAG_Y_CB_CR_SUB_SAMPLING, 530, IFD_FORMAT_USHORT),
new ExifTag(TAG_Y_CB_CR_POSITIONING, 531, IFD_FORMAT_USHORT),
new ExifTag(TAG_REFERENCE_BLACK_WHITE, 532, IFD_FORMAT_URATIONAL),
+ new ExifTag(TAG_XMP, 700, IFD_FORMAT_BYTE),
new ExifTag(TAG_COPYRIGHT, 33432, IFD_FORMAT_STRING),
new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG),
new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG),
@@ -3790,12 +3791,6 @@
new ExifTag(TAG_ORF_IMAGE_PROCESSING_IFD_POINTER, 8256, IFD_FORMAT_BYTE)
};
- // Tags for indicating the thumbnail offset and length
- private static final ExifTag JPEG_INTERCHANGE_FORMAT_TAG =
- new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT, 513, IFD_FORMAT_ULONG);
- private static final ExifTag JPEG_INTERCHANGE_FORMAT_LENGTH_TAG =
- new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 514, IFD_FORMAT_ULONG);
-
// Mappings from tag number to tag name and each item represents one IFD tag group.
@SuppressWarnings("unchecked")
private static final HashMap<Integer, ExifTag>[] sExifTagMapsForReading =
@@ -4669,7 +4664,7 @@
* other. It's best to use {@link #setAttribute(String,String)} to set all attributes to write
* and make a single call rather than multiple calls for each attribute.
* <p>
- * This method is supported for JPEG, PNG and WebP files.
+ * This method is supported for JPEG, PNG, WebP, and DNG formats.
* <p class="note">
* Note: after calling this method, any attempts to obtain range information
* from {@link #getAttributeRange(String)} or {@link #getThumbnailRange()}
@@ -4682,13 +4677,17 @@
*/
public void saveAttributes() throws IOException {
if (!isSupportedFormatForSavingAttributes(mMimeType)) {
- throw new IOException("ExifInterface only supports saving attributes on JPEG, PNG, "
- + "or WebP formats.");
+ throw new IOException("ExifInterface only supports saving attributes for JPEG, PNG, "
+ + "WebP, and DNG formats.");
}
if (mSeekableFileDescriptor == null && mFilename == null) {
throw new IOException(
"ExifInterface does not support saving attributes for the current input.");
}
+ if (mHasThumbnail && mHasThumbnailStrips && !mAreThumbnailStripsConsecutive) {
+ throw new IOException("ExifInterface does not support saving attributes when the image "
+ + "file has non-consecutive thumbnail strips");
+ }
// Remember the fact that we've changed the file on disk from what was
// originally parsed, meaning we can't answer range questions
@@ -4748,6 +4747,10 @@
savePngAttributes(bufferedIn, bufferedOut);
} else if (mMimeType == IMAGE_TYPE_WEBP) {
saveWebpAttributes(bufferedIn, bufferedOut);
+ } else if (mMimeType == IMAGE_TYPE_DNG || mMimeType == IMAGE_TYPE_UNKNOWN) {
+ ByteOrderedDataOutputStream dataOutputStream =
+ new ByteOrderedDataOutputStream(bufferedOut, ByteOrder.BIG_ENDIAN);
+ writeExifSegment(dataOutputStream);
}
} catch (Exception e) {
try {
@@ -7278,6 +7281,17 @@
if (!isThumbnail(mAttributes[IFD_TYPE_THUMBNAIL])) {
Log.d(TAG, "No image meets the size requirements of a thumbnail image.");
}
+
+ // TAG_THUMBNAIL_* tags should be replaced with TAG_* equivalents and vice versa if needed.
+ replaceInvalidTags(IFD_TYPE_PRIMARY, TAG_THUMBNAIL_ORIENTATION, TAG_ORIENTATION);
+ replaceInvalidTags(IFD_TYPE_PRIMARY, TAG_THUMBNAIL_IMAGE_LENGTH, TAG_IMAGE_LENGTH);
+ replaceInvalidTags(IFD_TYPE_PRIMARY, TAG_THUMBNAIL_IMAGE_WIDTH, TAG_IMAGE_WIDTH);
+ replaceInvalidTags(IFD_TYPE_PREVIEW, TAG_THUMBNAIL_ORIENTATION, TAG_ORIENTATION);
+ replaceInvalidTags(IFD_TYPE_PREVIEW, TAG_THUMBNAIL_IMAGE_LENGTH, TAG_IMAGE_LENGTH);
+ replaceInvalidTags(IFD_TYPE_PREVIEW, TAG_THUMBNAIL_IMAGE_WIDTH, TAG_IMAGE_WIDTH);
+ replaceInvalidTags(IFD_TYPE_THUMBNAIL, TAG_ORIENTATION, TAG_THUMBNAIL_ORIENTATION);
+ replaceInvalidTags(IFD_TYPE_THUMBNAIL, TAG_IMAGE_LENGTH, TAG_THUMBNAIL_IMAGE_LENGTH);
+ replaceInvalidTags(IFD_TYPE_THUMBNAIL, TAG_IMAGE_WIDTH, TAG_THUMBNAIL_IMAGE_WIDTH);
}
/**
@@ -7368,8 +7382,15 @@
removeAttribute(tag.name);
}
// Remove old thumbnail data
- removeAttribute(JPEG_INTERCHANGE_FORMAT_TAG.name);
- removeAttribute(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.name);
+ if (mHasThumbnail) {
+ if (mHasThumbnailStrips) {
+ removeAttribute(TAG_STRIP_OFFSETS);
+ removeAttribute(TAG_STRIP_BYTE_COUNTS);
+ } else {
+ removeAttribute(TAG_JPEG_INTERCHANGE_FORMAT);
+ removeAttribute(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
+ }
+ }
// Remove null value tags.
for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
@@ -7396,10 +7417,17 @@
ExifAttribute.createULong(0, mExifByteOrder));
}
if (mHasThumbnail) {
- mAttributes[IFD_TYPE_THUMBNAIL].put(JPEG_INTERCHANGE_FORMAT_TAG.name,
- ExifAttribute.createULong(0, mExifByteOrder));
- mAttributes[IFD_TYPE_THUMBNAIL].put(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.name,
- ExifAttribute.createULong(mThumbnailLength, mExifByteOrder));
+ if (mHasThumbnailStrips) {
+ mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_STRIP_OFFSETS,
+ ExifAttribute.createUShort(0, mExifByteOrder));
+ mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_STRIP_BYTE_COUNTS,
+ ExifAttribute.createUShort(mThumbnailLength, mExifByteOrder));
+ } else {
+ mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_JPEG_INTERCHANGE_FORMAT,
+ ExifAttribute.createULong(0, mExifByteOrder));
+ mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
+ ExifAttribute.createULong(mThumbnailLength, mExifByteOrder));
+ }
}
// Calculate IFD group data area sizes. IFD group data area is assigned to save the entry
@@ -7428,8 +7456,13 @@
}
if (mHasThumbnail) {
int thumbnailOffset = position;
- mAttributes[IFD_TYPE_THUMBNAIL].put(JPEG_INTERCHANGE_FORMAT_TAG.name,
- ExifAttribute.createULong(thumbnailOffset, mExifByteOrder));
+ if (mHasThumbnailStrips) {
+ mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_STRIP_OFFSETS,
+ ExifAttribute.createUShort(thumbnailOffset, mExifByteOrder));
+ } else {
+ mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_JPEG_INTERCHANGE_FORMAT,
+ ExifAttribute.createULong(thumbnailOffset, mExifByteOrder));
+ }
mThumbnailOffset = thumbnailOffset;
position += mThumbnailLength;
}
@@ -8017,6 +8050,16 @@
}
}
+ private void replaceInvalidTags(@IfdType int ifdType, String invalidTag, String validTag) {
+ if (!mAttributes[ifdType].isEmpty()) {
+ if (mAttributes[ifdType].get(invalidTag) != null) {
+ mAttributes[ifdType].put(validTag,
+ mAttributes[ifdType].get(invalidTag));
+ mAttributes[ifdType].remove(invalidTag);
+ }
+ }
+ }
+
/**
* Parsing EXIF data requires seek (moving to any position in the stream), so all MIME
* types should support seek via mark/reset, unless the MIME type specifies the position and
@@ -8033,7 +8076,8 @@
private static boolean isSupportedFormatForSavingAttributes(int mimeType) {
if (mimeType == IMAGE_TYPE_JPEG || mimeType == IMAGE_TYPE_PNG
- || mimeType == IMAGE_TYPE_WEBP) {
+ || mimeType == IMAGE_TYPE_WEBP || mimeType == IMAGE_TYPE_DNG
+ || mimeType == IMAGE_TYPE_UNKNOWN) {
return true;
}
return false;
diff --git a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/OnCreateDialogIncorrectCallbackDetectorTest.kt b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/OnCreateDialogIncorrectCallbackDetectorTest.kt
index 73405fd..66e3c38 100644
--- a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/OnCreateDialogIncorrectCallbackDetectorTest.kt
+++ b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/OnCreateDialogIncorrectCallbackDetectorTest.kt
@@ -26,7 +26,6 @@
/* ktlint-disable max-line-length */
@RunWith(JUnit4::class)
class OnCreateDialogIncorrectCallbackDetectorTest : LintDetectorTest() {
-
override fun getDetector(): Detector = OnCreateDialogIncorrectCallbackDetector()
override fun getIssues(): MutableList<Issue> {
@@ -180,6 +179,7 @@
@Test
fun `java expect fail dialog fragment with cancel listener`() {
lint().files(dialogFragmentStubJavaWithCancelListener)
+ .allowCompilationErrors(true) // b/193540422
.run()
.expect(
"""
@@ -195,6 +195,7 @@
@Test
fun `java expect fail dialog fragment with dismiss listener`() {
lint().files(dialogFragmentStubJavaWithDismissListener)
+ .allowCompilationErrors(true) // b/193540422
.run()
.expect(
"""
@@ -210,6 +211,7 @@
@Test
fun `java expect clean dialog fragment`() {
lint().files(dialogFragmentCorrectImplementationStubJava)
+ .allowCompilationErrors(true) // b/193540422
.run()
.expectClean()
}
@@ -217,6 +219,7 @@
@Test
fun `kotlin expect fail dialog fragment with cancel listener`() {
lint().files(dialogFragmentStubKotlinWithCancelListener)
+ .allowCompilationErrors(true) // b/193540422
.run()
.expect(
"""
@@ -232,6 +235,7 @@
@Test
fun `kotlin expect fail dialog fragment with dismiss listener`() {
lint().files(dialogFragmentStubKotlinWithDismissListener)
+ .allowCompilationErrors(true) // b/193540422
.run()
.expect(
"""
@@ -247,6 +251,7 @@
@Test
fun `kotlin expect fail dialog fragment with dismiss and cancel listeners`() {
lint().files(dialogFragmentStubKotlinWithDismissAndCancelListeners)
+ .allowCompilationErrors(true) // b/193540422
.run()
.expect(
"""
@@ -265,6 +270,7 @@
@Test
fun `kotlin expect clean dialog fragment`() {
lint().files(dialogFragmentCorrectImplementationStubKotlin)
+ .allowCompilationErrors(true) // b/193540422
.run()
.expectClean()
}
diff --git a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetectorTest.kt b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetectorTest.kt
index bb2084d..a083a19 100644
--- a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetectorTest.kt
+++ b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UnsafeRepeatOnLifecycleDetectorTest.kt
@@ -138,6 +138,7 @@
TestFiles.kt(TEMPLATE.format(config.fragmentExtensions, config.apiCall))
)
.issues(UnsafeRepeatOnLifecycleDetector.ISSUE)
+ .allowCompilationErrors(true) // b/193540422
.run()
if (config.shouldWarn) {
diff --git a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UseGetLayoutInflaterTest.kt b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UseGetLayoutInflaterTest.kt
index 6da2e21..61914ee 100644
--- a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UseGetLayoutInflaterTest.kt
+++ b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UseGetLayoutInflaterTest.kt
@@ -158,6 +158,7 @@
@Test
fun `java expect fail dialog fragment with fix`() {
lint().files(dialogFragmentStubJava)
+ .allowCompilationErrors(true) // b/193540422
.run()
.expect(
"""
@@ -181,6 +182,7 @@
@Test
fun `java expect clean non dialog fragment`() {
lint().files(fragmentStubJava)
+ .allowCompilationErrors(true) // b/193540422
.run()
.expectClean()
}
@@ -188,6 +190,7 @@
@Test
fun `java expect clean dialog fragment`() {
lint().files(dialogFragmentCorrectImplementationStubJava)
+ .allowCompilationErrors(true) // b/193540422
.run()
.expectClean()
}
@@ -195,6 +198,7 @@
@Test
fun `kotlin expect fail dialog fragment`() {
lint().files(dialogFragmentStubKotlin)
+ .allowCompilationErrors(true) // b/193540422
.run()
.expect(
"""
@@ -210,6 +214,7 @@
@Test
fun `kotlin expect clean non dialog fragment`() {
lint().files(fragmentStubKotlin)
+ .allowCompilationErrors(true) // b/193540422
.run()
.expectClean()
}
@@ -217,6 +222,7 @@
@Test
fun `kotlin expect clean dialog fragment`() {
lint().files(dialogFragmentCorrectImplementationStubKotlin)
+ .allowCompilationErrors(true) // b/193540422
.run()
.expectClean()
}
diff --git a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UseRequireInsteadOfGetTest.kt b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UseRequireInsteadOfGetTest.kt
index 5e96cad..a5080f7 100644
--- a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UseRequireInsteadOfGetTest.kt
+++ b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/UseRequireInsteadOfGetTest.kt
@@ -139,7 +139,7 @@
"""
).indented()
)
- .allowCompilationErrors(false)
+ .allowCompilationErrors(true) // b/193540422
.run()
.expect(
"""
@@ -255,7 +255,7 @@
"""
).indented()
)
- .allowCompilationErrors(false)
+ .allowCompilationErrors(true) // b/193540422
.run()
.expect(
"""
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index d672ed1..08071e9 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -28,7 +28,7 @@
api("androidx.collection:collection:1.1.0")
api("androidx.viewpager:viewpager:1.0.0")
api("androidx.loader:loader:1.0.0")
- api("androidx.activity:activity:1.2.3")
+ api("androidx.activity:activity:1.2.4")
api("androidx.lifecycle:lifecycle-livedata-core:2.3.1")
api("androidx.lifecycle:lifecycle-viewmodel:2.3.1")
api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1")
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..b27044f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,13 +2,13 @@
# -----------------------------------------------------------------------------
# All of the following should be updated in sync.
# -----------------------------------------------------------------------------
-androidGradlePlugin = "7.1.0-alpha02"
+androidGradlePlugin = "7.1.0-alpha03"
# NOTE: When updating the lint version we also need to update the `api` version
# supported by `IssueRegistry`'s.' For e.g. r.android.com/1331903
-androidLint = "30.1.0-alpha02"
+androidLint = "30.1.0-alpha03"
# Once you have a chosen version of AGP to upgrade to, go to
# https://developer.android.com/studio/archive and find the matching version of Studio.
-androidStudio = "2021.1.1.2"
+androidStudio = "2021.1.1.3"
# -----------------------------------------------------------------------------
androidLintMin = "27.2.1"
@@ -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/hilt/hilt-navigation-compose/samples/build.gradle b/hilt/hilt-navigation-compose/samples/build.gradle
index 0a3b526..a048a87 100644
--- a/hilt/hilt-navigation-compose/samples/build.gradle
+++ b/hilt/hilt-navigation-compose/samples/build.gradle
@@ -17,6 +17,7 @@
import androidx.build.LibraryGroups
import androidx.build.LibraryVersions
import androidx.build.LibraryType
+import androidx.build.Publish
plugins {
id("AndroidXPlugin")
@@ -35,6 +36,7 @@
androidx {
name = "Navigation Compose Hilt Extension Samples"
+ publish = Publish.SNAPSHOT_AND_RELEASE
type = LibraryType.SAMPLES
mavenGroup = LibraryGroups.HILT
mavenVersion = LibraryVersions.HILT_NAVIGATION_COMPOSE
diff --git a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt
index 4f63abc..3092ddb 100644
--- a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt
+++ b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt
@@ -53,6 +53,12 @@
it.setupInspectorAttribute()
}
+ val publishNonDexedInspector = project.configurations.create("publishNonDexedInspector") {
+ it.isCanBeConsumed = true
+ it.isCanBeResolved = false
+ it.setupNonDexedInspectorAttribute()
+ }
+
project.pluginManager.withPlugin("com.android.library") {
foundLibraryPlugin = true
val libExtension = project.extensions.getByType(LibraryExtension::class.java)
@@ -61,11 +67,18 @@
if (variant.name == "release") {
foundReleaseVariant = true
val unzip = project.registerUnzipTask(variant)
- val shadowJar = project.registerShadowDependenciesTask(variant, unzip)
+ val shadowJar = project.registerShadowDependenciesTask(
+ variant, extension.name, unzip
+ )
val bundleTask = project.registerBundleInspectorTask(
variant, libExtension, extension.name, shadowJar
)
+ publishNonDexedInspector.outgoing.variants {
+ val configVariant = it.create("inspectorNonDexedJar")
+ configVariant.artifact(shadowJar)
+ }
+
publishInspector.outgoing.variants {
val configVariant = it.create("inspectorJar")
configVariant.artifact(bundleTask)
@@ -169,6 +182,17 @@
}
}
+fun Project.createConsumeNonDexedInspectionConfiguration(): Configuration =
+ configurations.create("consumeNonDexedInspector") {
+ it.setupNonDexedInspectorAttribute()
+ }
+
+private fun Configuration.setupNonDexedInspectorAttribute() {
+ attributes {
+ it.attribute(Attribute.of("inspector-undexed", String::class.java), "inspectorUndexedJar")
+ }
+}
+
@ExperimentalStdlibApi
private fun generateProguardDetectionFile(libraryProject: Project) {
val libExtension = libraryProject.extensions.getByType(LibraryExtension::class.java)
diff --git a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/ShadowDependenciesTask.kt b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/ShadowDependenciesTask.kt
index a284800..580468c 100644
--- a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/ShadowDependenciesTask.kt
+++ b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/ShadowDependenciesTask.kt
@@ -37,6 +37,7 @@
@Suppress("DEPRECATION") // BaseVariant
fun Project.registerShadowDependenciesTask(
variant: com.android.build.gradle.api.BaseVariant,
+ jarName: String?,
zipTask: TaskProvider<Copy>
): TaskProvider<ShadowJar> {
val uberJar = registerUberJarTask(variant)
@@ -60,7 +61,8 @@
it.transform(RenameServicesTransformer::class.java)
it.from(versionTask.get().outputDir)
it.destinationDirectory.set(taskWorkingDir(variant, "shadowedJar"))
- it.archiveBaseName.set("${project.name}-shadowed")
+ it.archiveBaseName.set("${jarName ?: project.name}-nondexed")
+ it.archiveVersion.set("")
it.dependsOn(zipTask)
val prefix = "deps.${project.name.replace('-', '.')}"
it.doFirst {
@@ -86,6 +88,7 @@
it.dependsOn(variant.assembleProvider)
it.archiveClassifier.set("uberRuntimeDepsJar")
it.exclude("**/module-info.class")
+ it.exclude("**/*.proto")
it.exclude("META-INF/versions/9/**/*.class")
it.from({
variant.runtimeConfiguration.incoming.artifactView {
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/BaseFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/BaseFragment.java
index 1dccd30..7b50326 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/BaseFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/BaseFragment.java
@@ -214,11 +214,13 @@
* <p>
* It is similar to android:windowsEnterTransition and can be considered a late-executed
* android:windowsEnterTransition controlled by app. There are two reasons that app needs it:
+ * <ul>
* <li> Workaround the problem that activity transition is not available between launcher and
* app. Browse activity must programmatically start the slide-in transition.</li>
* <li> Separates DetailsOverviewRow transition from other rows transition. So that
* the DetailsOverviewRow transition can be executed earlier without waiting for all rows
* to be loaded.</li>
+ * </ul>
* <p>
* Transition object is returned by createEntranceTransition(). Typically the app does not need
* override the default transition that browse and details provides.
@@ -269,8 +271,10 @@
* to execute the entrance transition.
* startEntranceTransition() will start transition only if both two conditions
* are satisfied:
+ * <ul>
* <li> prepareEntranceTransition() was called.</li>
* <li> has not executed entrance transition yet.</li>
+ * </ul>
* <p>
* If startEntranceTransition() is called before onViewCreated(), it will be pending
* and executed when view is created.
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/BaseSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/BaseSupportFragment.java
index 783a8ea..1798d4d2 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/BaseSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/BaseSupportFragment.java
@@ -209,11 +209,13 @@
* <p>
* It is similar to android:windowsEnterTransition and can be considered a late-executed
* android:windowsEnterTransition controlled by app. There are two reasons that app needs it:
+ * <ul>
* <li> Workaround the problem that activity transition is not available between launcher and
* app. Browse activity must programmatically start the slide-in transition.</li>
* <li> Separates DetailsOverviewRow transition from other rows transition. So that
* the DetailsOverviewRow transition can be executed earlier without waiting for all rows
* to be loaded.</li>
+ * </ul>
* <p>
* Transition object is returned by createEntranceTransition(). Typically the app does not need
* override the default transition that browse and details provides.
@@ -264,8 +266,10 @@
* to execute the entrance transition.
* startEntranceTransition() will start transition only if both two conditions
* are satisfied:
+ * <ul>
* <li> prepareEntranceTransition() was called.</li>
* <li> has not executed entrance transition yet.</li>
+ * </ul>
* <p>
* If startEntranceTransition() is called before onViewCreated(), it will be pending
* and executed when view is created.
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/DetailsFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/DetailsFragment.java
index 9cbfbfd..fd91afb 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/DetailsFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/DetailsFragment.java
@@ -65,6 +65,7 @@
*
* When {@link FullWidthDetailsOverviewRowPresenter} is found in adapter, DetailsFragment will
* setup default behavior of the DetailsOverviewRow:
+ * <ul>
* <li>
* The alignment of FullWidthDetailsOverviewRowPresenter is setup in
* {@link #setupDetailsOverviewRowPresenter(FullWidthDetailsOverviewRowPresenter)}.
@@ -74,9 +75,11 @@
* {@link #onSetDetailsOverviewRowStatus(FullWidthDetailsOverviewRowPresenter,
* FullWidthDetailsOverviewRowPresenter.ViewHolder, int, int, int)}.
* </li>
+ * </ul>
*
* <p>
* The recommended activity themes to use with a DetailsFragment are
+ * <ul>
* <li>
* {@link androidx.leanback.R.style#Theme_Leanback_Details} with activity
* shared element transition for {@link FullWidthDetailsOverviewRowPresenter}.
@@ -86,6 +89,7 @@
* if shared element transition is not needed, for example if first row is not rendered by
* {@link FullWidthDetailsOverviewRowPresenter}.
* </li>
+ * </ul>
* </p>
*
* <p>
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/DetailsSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/DetailsSupportFragment.java
index f189253..e54495e 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/DetailsSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/DetailsSupportFragment.java
@@ -62,6 +62,7 @@
*
* When {@link FullWidthDetailsOverviewRowPresenter} is found in adapter, DetailsSupportFragment will
* setup default behavior of the DetailsOverviewRow:
+ * <ul>
* <li>
* The alignment of FullWidthDetailsOverviewRowPresenter is setup in
* {@link #setupDetailsOverviewRowPresenter(FullWidthDetailsOverviewRowPresenter)}.
@@ -71,9 +72,11 @@
* {@link #onSetDetailsOverviewRowStatus(FullWidthDetailsOverviewRowPresenter,
* FullWidthDetailsOverviewRowPresenter.ViewHolder, int, int, int)}.
* </li>
+ * </ul>
*
* <p>
* The recommended activity themes to use with a DetailsSupportFragment are
+ * <ul>
* <li>
* {@link androidx.leanback.R.style#Theme_Leanback_Details} with activity
* shared element transition for {@link FullWidthDetailsOverviewRowPresenter}.
@@ -83,6 +86,7 @@
* if shared element transition is not needed, for example if first row is not rendered by
* {@link FullWidthDetailsOverviewRowPresenter}.
* </li>
+ * </ul>
* </p>
*
* <p>
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/GuidedStepFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/GuidedStepFragment.java
index 4f33a81..8c5573f 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/GuidedStepFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/GuidedStepFragment.java
@@ -80,10 +80,10 @@
* GuidedStepFragment, int)}, to add GuidedStepFragment on top of existing Fragments or
* replacing existing GuidedStepFragment when moving forward to next step.</li>
* <li>{@link #finishGuidedStepFragments()} can either finish the activity or pop all
- * GuidedStepFragment from stack.
+ * GuidedStepFragment from stack.</li>
* <li>If app chooses not to use the helper function, it is the app's responsibility to call
* {@link #setUiStyle(int)} to select fragment transition and remember the stack entry where it
- * need pops to.
+ * need pops to.</li>
* </ul>
* <h3>Theming and Stylists</h3>
* <p>
@@ -468,8 +468,11 @@
* GuidedStepFragments in the stack, and configuring the fragment-to-fragment custom
* transitions. A backstack entry is added, so the fragment will be dismissed when BACK key
* is pressed.
- * <li>If current fragment on stack is GuidedStepFragment: assign {@link #UI_STYLE_REPLACE}
- * <li>If current fragment on stack is not GuidedStepFragment: assign {@link #UI_STYLE_ENTRANCE}
+ * <ul>
+ * <li>If current fragment on stack is GuidedStepFragment: assign {@link #UI_STYLE_REPLACE}</li>
+ * <li>If current fragment on stack is not GuidedStepFragment: assign
+ * {@link #UI_STYLE_ENTRANCE}</li>
+ * </ul>
* <p>
* Note: currently fragments added using this method must be created programmatically rather
* than via XML.
@@ -486,10 +489,13 @@
* GuidedStepFragments in the stack, and configuring the fragment-to-fragment custom
* transitions. A backstack entry is added, so the fragment will be dismissed when BACK key
* is pressed.
+ * <ul>
* <li>If current fragment on stack is GuidedStepFragment: assign {@link #UI_STYLE_REPLACE} and
* {@link #onAddSharedElementTransition(FragmentTransaction, GuidedStepFragment)} will be called
- * to perform shared element transition between GuidedStepFragments.
- * <li>If current fragment on stack is not GuidedStepFragment: assign {@link #UI_STYLE_ENTRANCE}
+ * to perform shared element transition between GuidedStepFragments.</li>
+ * <li>If current fragment on stack is not GuidedStepFragment: assign
+ * {@link #UI_STYLE_ENTRANCE}</li>
+ * </ul>
* <p>
* Note: currently fragments added using this method must be created programmatically rather
* than via XML.
@@ -875,12 +881,12 @@
* transitions based on {@link #getUiStyle()}:
* <ul>
* <li> {@link #UI_STYLE_REPLACE} Slide from/to end(right) for enter transition, slide from/to
- * start(left) for exit transition, shared element enter transition is set to ChangeBounds.
+ * start(left) for exit transition, shared element enter transition is set to ChangeBounds.</li>
* <li> {@link #UI_STYLE_ENTRANCE} Enter transition is set to slide from both sides, exit
- * transition is same as {@link #UI_STYLE_REPLACE}, no shared element enter transition.
+ * transition is same as {@link #UI_STYLE_REPLACE}, no shared element enter transition.</li>
* <li> {@link #UI_STYLE_ACTIVITY_ROOT} Enter transition is set to null and app should rely on
* activity transition, exit transition is same as {@link #UI_STYLE_REPLACE}, no shared element
- * enter transition.
+ * enter transition.</li>
* </ul>
* <p>
* The default implementation heavily relies on {@link GuidedActionsStylist} and
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/GuidedStepSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/GuidedStepSupportFragment.java
index 4cc9a63..383ade9 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/GuidedStepSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/GuidedStepSupportFragment.java
@@ -77,10 +77,10 @@
* GuidedStepSupportFragment, int)}, to add GuidedStepSupportFragment on top of existing Fragments or
* replacing existing GuidedStepSupportFragment when moving forward to next step.</li>
* <li>{@link #finishGuidedStepSupportFragments()} can either finish the activity or pop all
- * GuidedStepSupportFragment from stack.
+ * GuidedStepSupportFragment from stack.</li>
* <li>If app chooses not to use the helper function, it is the app's responsibility to call
* {@link #setUiStyle(int)} to select fragment transition and remember the stack entry where it
- * need pops to.
+ * need pops to.</li>
* </ul>
* <h3>Theming and Stylists</h3>
* <p>
@@ -463,8 +463,16 @@
* GuidedStepSupportFragments in the stack, and configuring the fragment-to-fragment custom
* transitions. A backstack entry is added, so the fragment will be dismissed when BACK key
* is pressed.
- * <li>If current fragment on stack is GuidedStepSupportFragment: assign {@link #UI_STYLE_REPLACE}
- * <li>If current fragment on stack is not GuidedStepSupportFragment: assign {@link #UI_STYLE_ENTRANCE}
+ * <ul>
+ * <li>
+ * If current fragment on stack is GuidedStepSupportFragment: assign
+ * {@link #UI_STYLE_REPLACE}
+ * </li>
+ * <li>
+ * If current fragment on stack is not GuidedStepSupportFragment: assign
+ * {@link #UI_STYLE_ENTRANCE}
+ * </li>
+ * </ul>
* <p>
* Note: currently fragments added using this method must be created programmatically rather
* than via XML.
@@ -481,10 +489,18 @@
* GuidedStepSupportFragments in the stack, and configuring the fragment-to-fragment custom
* transitions. A backstack entry is added, so the fragment will be dismissed when BACK key
* is pressed.
- * <li>If current fragment on stack is GuidedStepSupportFragment: assign {@link #UI_STYLE_REPLACE} and
- * {@link #onAddSharedElementTransition(FragmentTransaction, GuidedStepSupportFragment)} will be called
- * to perform shared element transition between GuidedStepSupportFragments.
- * <li>If current fragment on stack is not GuidedStepSupportFragment: assign {@link #UI_STYLE_ENTRANCE}
+ * <ul>
+ * <li>
+ * If current fragment on stack is GuidedStepSupportFragment: assign
+ * {@link #UI_STYLE_REPLACE} and
+ * {@link #onAddSharedElementTransition(FragmentTransaction, GuidedStepSupportFragment)}
+ * will be called to perform shared element transition between GuidedStepSupportFragments.
+ * </li>
+ * <li>
+ * If current fragment on stack is not GuidedStepSupportFragment: assign
+ * {@link #UI_STYLE_ENTRANCE}
+ * </li>
+ * </ul>
* <p>
* Note: currently fragments added using this method must be created programmatically rather
* than via XML.
@@ -870,12 +886,12 @@
* transitions based on {@link #getUiStyle()}:
* <ul>
* <li> {@link #UI_STYLE_REPLACE} Slide from/to end(right) for enter transition, slide from/to
- * start(left) for exit transition, shared element enter transition is set to ChangeBounds.
+ * start(left) for exit transition, shared element enter transition is set to ChangeBounds.</li>
* <li> {@link #UI_STYLE_ENTRANCE} Enter transition is set to slide from both sides, exit
- * transition is same as {@link #UI_STYLE_REPLACE}, no shared element enter transition.
+ * transition is same as {@link #UI_STYLE_REPLACE}, no shared element enter transition.</li>
* <li> {@link #UI_STYLE_ACTIVITY_ROOT} Enter transition is set to null and app should rely on
* activity transition, exit transition is same as {@link #UI_STYLE_REPLACE}, no shared element
- * enter transition.
+ * enter transition.</li>
* </ul>
* <p>
* The default implementation heavily relies on {@link GuidedActionsStylist} and
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/PlaybackFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/PlaybackFragment.java
index e1fdbf0..a1e521e 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/PlaybackFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/PlaybackFragment.java
@@ -86,11 +86,12 @@
* <li>
* App may manually call {@link #showControlsOverlay(boolean)} or
* {@link #hideControlsOverlay(boolean)} to show or hide the controls.
- * <li>
+ * </li>
* <li>
* The controls are visible by default upon onViewCreated(). To make it initially invisible,
* call hideControlsOverlay(false) in overridden onViewCreated().
* </li>
+ * <li>
* Upon play or pause, PlaybackControlGlue or PlaybackTransportControlGlue will fade-in
* the controls and automatically fade out after a delay customized by
* {@link R.attr#playbackControlsAutoHideTimeout}. To disable the fade in and fade out
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/PlaybackSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/PlaybackSupportFragment.java
index 8f7dd41..fc4585b 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/PlaybackSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/PlaybackSupportFragment.java
@@ -83,11 +83,12 @@
* <li>
* App may manually call {@link #showControlsOverlay(boolean)} or
* {@link #hideControlsOverlay(boolean)} to show or hide the controls.
- * <li>
+ * </li>
* <li>
* The controls are visible by default upon onViewCreated(). To make it initially invisible,
* call hideControlsOverlay(false) in overridden onViewCreated().
* </li>
+ * <li>
* Upon play or pause, PlaybackControlGlue or PlaybackTransportControlGlue will fade-in
* the controls and automatically fade out after a delay customized by
* {@link R.attr#playbackControlsAutoHideTimeout}. To disable the fade in and fade out
diff --git a/leanback/leanback/src/main/java/androidx/leanback/media/MediaControllerAdapter.java b/leanback/leanback/src/main/java/androidx/leanback/media/MediaControllerAdapter.java
index 3df20ff..8e10f9f 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/media/MediaControllerAdapter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/media/MediaControllerAdapter.java
@@ -273,9 +273,9 @@
}
@Override
- public void setProgressUpdatingEnabled(boolean enabled) {
+ public void setProgressUpdatingEnabled(boolean enable) {
mHandler.removeCallbacks(mPositionUpdaterRunnable);
- if (!enabled) {
+ if (!enable) {
return;
}
mHandler.postDelayed(mPositionUpdaterRunnable, getUpdatePeriod());
diff --git a/leanback/leanback/src/main/java/androidx/leanback/media/MediaPlayerAdapter.java b/leanback/leanback/src/main/java/androidx/leanback/media/MediaPlayerAdapter.java
index 866ae94..e83db83 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/media/MediaPlayerAdapter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/media/MediaPlayerAdapter.java
@@ -190,17 +190,17 @@
*
* @param what the type of error that has occurred:
* <ul>
- * <li>{@link MediaPlayer#MEDIA_ERROR_UNKNOWN}
- * <li>{@link MediaPlayer#MEDIA_ERROR_SERVER_DIED}
+ * <li>{@link MediaPlayer#MEDIA_ERROR_UNKNOWN}</li>
+ * <li>{@link MediaPlayer#MEDIA_ERROR_SERVER_DIED}</li>
* </ul>
* @param extra an extra code, specific to the error. Typically
* implementation dependent.
* <ul>
- * <li>{@link MediaPlayer#MEDIA_ERROR_IO}
- * <li>{@link MediaPlayer#MEDIA_ERROR_MALFORMED}
- * <li>{@link MediaPlayer#MEDIA_ERROR_UNSUPPORTED}
- * <li>{@link MediaPlayer#MEDIA_ERROR_TIMED_OUT}
- * <li><code>MEDIA_ERROR_SYSTEM (-2147483648)</code> - low-level system error.
+ * <li>{@link MediaPlayer#MEDIA_ERROR_IO}</li>
+ * <li>{@link MediaPlayer#MEDIA_ERROR_MALFORMED}</li>
+ * <li>{@link MediaPlayer#MEDIA_ERROR_UNSUPPORTED}</li>
+ * <li>{@link MediaPlayer#MEDIA_ERROR_TIMED_OUT}</li>
+ * <li><code>MEDIA_ERROR_SYSTEM (-2147483648)</code> - low-level system error.</li>
* </ul>
* @return True if the method handled the error, false if it didn't.
* Returning false, will cause the {@link PlayerAdapter.Callback#onPlayCompleted(PlayerAdapter)}
@@ -221,18 +221,18 @@
*
* @param what the type of info or warning.
* <ul>
- * <li>{@link MediaPlayer#MEDIA_INFO_UNKNOWN}
- * <li>{@link MediaPlayer#MEDIA_INFO_VIDEO_TRACK_LAGGING}
- * <li>{@link MediaPlayer#MEDIA_INFO_VIDEO_RENDERING_START}
- * <li>{@link MediaPlayer#MEDIA_INFO_BUFFERING_START}
- * <li>{@link MediaPlayer#MEDIA_INFO_BUFFERING_END}
+ * <li>{@link MediaPlayer#MEDIA_INFO_UNKNOWN}</li>
+ * <li>{@link MediaPlayer#MEDIA_INFO_VIDEO_TRACK_LAGGING}</li>
+ * <li>{@link MediaPlayer#MEDIA_INFO_VIDEO_RENDERING_START}</li>
+ * <li>{@link MediaPlayer#MEDIA_INFO_BUFFERING_START}</li>
+ * <li>{@link MediaPlayer#MEDIA_INFO_BUFFERING_END}</li>
* <li><code>MEDIA_INFO_NETWORK_BANDWIDTH (703)</code> -
- * bandwidth information is available (as <code>extra</code> kbps)
- * <li>{@link MediaPlayer#MEDIA_INFO_BAD_INTERLEAVING}
- * <li>{@link MediaPlayer#MEDIA_INFO_NOT_SEEKABLE}
- * <li>{@link MediaPlayer#MEDIA_INFO_METADATA_UPDATE}
- * <li>{@link MediaPlayer#MEDIA_INFO_UNSUPPORTED_SUBTITLE}
- * <li>{@link MediaPlayer#MEDIA_INFO_SUBTITLE_TIMED_OUT}
+ * bandwidth information is available (as <code>extra</code> kbps)</li>
+ * <li>{@link MediaPlayer#MEDIA_INFO_BAD_INTERLEAVING}</li>
+ * <li>{@link MediaPlayer#MEDIA_INFO_NOT_SEEKABLE}</li>
+ * <li>{@link MediaPlayer#MEDIA_INFO_METADATA_UPDATE}</li>
+ * <li>{@link MediaPlayer#MEDIA_INFO_UNSUPPORTED_SUBTITLE}</li>
+ * <li>{@link MediaPlayer#MEDIA_INFO_SUBTITLE_TIMED_OUT}</li>
* </ul>
* @param extra an extra code, specific to the info. Typically
* implementation dependent.
@@ -266,9 +266,9 @@
}
@Override
- public void setProgressUpdatingEnabled(final boolean enabled) {
+ public void setProgressUpdatingEnabled(final boolean enable) {
mHandler.removeCallbacks(mRunnable);
- if (!enabled) {
+ if (!enable) {
return;
}
mHandler.postDelayed(mRunnable, getProgressUpdatingInterval());
@@ -316,11 +316,11 @@
}
@Override
- public void seekTo(long newPosition) {
+ public void seekTo(long positionInMs) {
if (!mInitialized) {
return;
}
- mPlayer.seekTo((int) newPosition);
+ mPlayer.seekTo((int) positionInMs);
}
@Override
diff --git a/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackGlueHost.java b/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackGlueHost.java
index 0ac60da..98c8d0a 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackGlueHost.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/media/PlaybackGlueHost.java
@@ -27,6 +27,7 @@
* This class represents the UI (e.g. Fragment/Activity) hosting playback controls and
* defines the interaction between {@link PlaybackGlue} and the host.
* PlaybackGlueHost provides the following functions:
+ * <ul>
* <li>Render UI of PlaybackGlue: {@link #setPlaybackRow(Row)},
* {@link #setPlaybackRowPresenter(PlaybackRowPresenter)}.
* </li>
@@ -37,10 +38,13 @@
* <li>Key listener and ActionListener. {@link #setOnKeyInterceptListener(View.OnKeyListener)},
* {@link #setOnActionClickedListener(OnActionClickedListener)}.
* </li>
+ * </ul>
*
* Subclass of PlaybackGlueHost may implement optional interfaces:
+ * <ul>
* <li>{@link SurfaceHolderGlueHost} to provide SurfaceView for video playback.</li>
* <li>{@link PlaybackSeekUi} to provide seek UI to glue</li>
+ * </ul>
* These optional interfaces should be accessed by glue in
* {@link PlaybackGlue#onAttachedToHost(PlaybackGlueHost)}.
*/
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/BaseOnItemViewSelectedListener.java b/leanback/leanback/src/main/java/androidx/leanback/widget/BaseOnItemViewSelectedListener.java
index e060516..cd5d338 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/BaseOnItemViewSelectedListener.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/BaseOnItemViewSelectedListener.java
@@ -31,6 +31,7 @@
* <p>
* In the case of a grid, the row parameter is always null.
* </p>
+ * <ul>
* <li>
* Row has focus: event is fired when focus changes between children of the row.
* </li>
@@ -38,6 +39,7 @@
* No row has focus: the event is fired with the currently selected row and last
* focused item in the row.
* </li>
+ * </ul>
*
* @param itemViewHolder The view holder of the item that is currently selected.
* @param item The item that is currently selected.
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/DetailsParallaxDrawable.java b/leanback/leanback/src/main/java/androidx/leanback/widget/DetailsParallaxDrawable.java
index 80e9dbf..96f0cda 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/DetailsParallaxDrawable.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/DetailsParallaxDrawable.java
@@ -52,10 +52,10 @@
* Call {@link #DetailsParallaxDrawable(Context, DetailsParallax)} to create DetailsParallaxDrawable
* using {@link FitWidthBitmapDrawable} for cover drawable.
* </li>
- * </ul>
* <li>
* In case the solid color is not set, it will use defaultBrandColorDark from LeanbackTheme.
* </li>
+ * </ul>
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/FacetProvider.java b/leanback/leanback/src/main/java/androidx/leanback/widget/FacetProvider.java
index c1ca54a..3287bea 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/FacetProvider.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/FacetProvider.java
@@ -21,6 +21,7 @@
* VerticalGridView or HorizontalGridView.
* A FacetProvider could be retrieved from two sources by VerticalGridView/HorizontalGridView in
* the following order.
+ * <ul>
* <li>
* <p>
* ViewHolder based facet:
@@ -53,6 +54,7 @@
* set alignment of all ViewHolders created by this Presenter.
* </p>
* </li>
+ * </ul>
*/
public interface FacetProvider {
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/GridLayoutManager.java b/leanback/leanback/src/main/java/androidx/leanback/widget/GridLayoutManager.java
index c4bfc64..d12f76d3 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/GridLayoutManager.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/GridLayoutManager.java
@@ -2933,8 +2933,8 @@
@Override
public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent,
- @NonNull View view, @NonNull Rect rect, boolean immediate) {
- if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect);
+ @NonNull View child, @NonNull Rect rect, boolean immediate) {
+ if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + child + " " + rect);
return false;
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedAction.java b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedAction.java
index 952d62e..c2857b2 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedAction.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedAction.java
@@ -903,10 +903,12 @@
* Returns true if Action will be saved to instanceState and restored later, false otherwise.
* The default value is true. When isAutoSaveRestoreEnabled() is true and {@link #getId()} is
* not {@link #NO_ID}:
+ * <ul>
* <li>{@link #isEditable()} is true: save text of {@link #getTitle()}</li>
* <li>{@link #isDescriptionEditable()} is true: save text of {@link #getDescription()}</li>
* <li>{@link #getCheckSetId()} is not {@link #NO_CHECK_SET}: save {@link #isChecked()}}</li>
* <li>{@link GuidedDatePickerAction} will be saved</li>
+ * </ul>
* App may explicitly disable auto restore and handle by itself. App should override Fragment
* onSaveInstanceState() and onCreateActions()
* @return True if Action will be saved to instanceState and restored later, false otherwise.
@@ -917,10 +919,12 @@
/**
* Save action into a bundle using a given key. When isAutoRestoreEna() is true:
+ * <ul>
* <li>{@link #isEditable()} is true: save text of {@link #getTitle()}</li>
* <li>{@link #isDescriptionEditable()} is true: save text of {@link #getDescription()}</li>
* <li>{@link #getCheckSetId()} is not {@link #NO_CHECK_SET}: save {@link #isChecked()}}</li>
* <li>{@link GuidedDatePickerAction} will be saved</li>
+ * </ul>
* Subclass may override this method.
* @param bundle Bundle to save the Action.
* @param key Key used to save the Action.
@@ -937,10 +941,12 @@
/**
* Restore action from a bundle using a given key. When isAutoRestore() is true:
+ * <ul>
* <li>{@link #isEditable()} is true: save text of {@link #getTitle()}</li>
* <li>{@link #isDescriptionEditable()} is true: save text of {@link #getDescription()}</li>
* <li>{@link #getCheckSetId()} is not {@link #NO_CHECK_SET}: save {@link #isChecked()}}</li>
* <li>{@link GuidedDatePickerAction} will be saved</li>
+ * </ul>
* Subclass may override this method.
* @param bundle Bundle to restore the Action from.
* @param key Key used to restore the Action.
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionsStylist.java b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionsStylist.java
index d13fb77..5c09223 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionsStylist.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/GuidedActionsStylist.java
@@ -83,12 +83,18 @@
* {@link #getItemViewType(GuidedAction)} method to change the layout used to display each action.
* <p>
* To support a "click to activate" view similar to DatePicker, app needs:
+ * <ul>
* <li> Override {@link #onProvideItemLayoutId(int)} and {@link #getItemViewType(GuidedAction)},
- * provides a layout id for the action.
+ * provides a layout id for the action.</li>
* <li> The layout must include a widget with id "guidedactions_activator_item", the widget is
- * toggled edit mode by {@link View#setActivated(boolean)}.
- * <li> Override {@link #onBindActivatorView(ViewHolder, GuidedAction)} to populate values into View.
- * <li> Override {@link #onUpdateActivatorView(ViewHolder, GuidedAction)} to update action.
+ * toggled edit mode by {@link View#setActivated(boolean)}.</li>
+ * <li>
+ * Override {@link #onBindActivatorView(ViewHolder, GuidedAction)} to populate values into View.
+ * </li>
+ * <li>
+ * Override {@link #onUpdateActivatorView(ViewHolder, GuidedAction)} to update action.
+ * </li>
+ * </ul>
* <p>
* Note: If an alternate list layout is provided, the following view IDs must be supplied:
* <ul>
@@ -612,10 +618,12 @@
* Provides the resource ID of the layout defining the view for an individual guided actions.
* Subclasses may override to provide their own customized layouts. The base implementation
* supports:
- * <li>{@link androidx.leanback.R.layout#lb_guidedactions_item}
- * <li>{{@link androidx.leanback.R.layout#lb_guidedactions_datepicker_item}. If
- * overridden, the substituted layout should contain matching IDs for any views that should be
- * managed by the base class; this can be achieved by starting with a copy of the base layout
+ * <ul>
+ * <li>{@link androidx.leanback.R.layout#lb_guidedactions_item}</li>
+ * <li>{{@link androidx.leanback.R.layout#lb_guidedactions_datepicker_item}.</li>
+ * </ul>
+ * If overridden, the substituted layout should contain matching IDs for any views that should
+ * be managed by the base class; this can be achieved by starting with a copy of the base layout
* file. Note that in order for the item to support editing, the title view should both subclass
* {@link android.widget.EditText} and implement {@link ImeKeyMonitor}; see
* {@link GuidedActionEditText}.
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/OnChildViewHolderSelectedListener.java b/leanback/leanback/src/main/java/androidx/leanback/widget/OnChildViewHolderSelectedListener.java
index 0f13772..d636008 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/OnChildViewHolderSelectedListener.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/OnChildViewHolderSelectedListener.java
@@ -19,6 +19,7 @@
/**
* Interface for receiving notification when a child of this ViewGroup has been selected.
* There are two methods:
+ * <ul>
* <li>
* {link {@link #onChildViewHolderSelected(RecyclerView, RecyclerView.ViewHolder, int, int)}}
* is called when the view holder is about to be selected. The listener could change size
@@ -29,6 +30,7 @@
* int, int)} is called when view holder has been selected and laid out in RecyclerView.
*
* </li>
+ * </ul>
*/
public abstract class OnChildViewHolderSelectedListener {
/**
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/Parallax.java b/leanback/leanback/src/main/java/androidx/leanback/widget/Parallax.java
index 695cf03..3433eec 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/Parallax.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/Parallax.java
@@ -55,7 +55,8 @@
* a great example of Parallax implementation tracking child view positions on screen.
* </p>
* <p>
- * <ul>Restrictions of properties
+ * Restrictions of properties
+ * <ul>
* <li>FloatProperty and IntProperty cannot be mixed in one Parallax</li>
* <li>Values must be in ascending order.</li>
* <li>If the UI element is unknown above screen, use UNKNOWN_BEFORE.</li>
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/Presenter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/Presenter.java
index 75b357a..949faa0 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/Presenter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/Presenter.java
@@ -66,7 +66,7 @@
* </pre>
* In addition to view creation and binding, Presenter allows dynamic interface (facet) to
* be added: {@link #setFacet(Class, Object)}. Supported facets:
- * <li> {@link ItemAlignmentFacet} is used by {@link HorizontalGridView} and
+ * {@link ItemAlignmentFacet} is used by {@link HorizontalGridView} and
* {@link VerticalGridView} to customize child alignment.
*/
public abstract class Presenter implements FacetProvider {
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/Row.java b/leanback/leanback/src/main/java/androidx/leanback/widget/Row.java
index 760ef47..f14c546 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/Row.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/Row.java
@@ -91,10 +91,10 @@
* three places:
* <ul>
* <li>If {@link #setId(long)} is ever called on this row, it will return
- * this id.
+ * this id.</li>
* <li>If {@link #setId(long)} has not been called but the header item is
- * not null, the result of {@link HeaderItem#getId()} is returned.
- * <li>Otherwise {@link ObjectAdapter#NO_ID NO_ID} is returned.
+ * not null, the result of {@link HeaderItem#getId()} is returned.</li>
+ * <li>Otherwise {@link ObjectAdapter#NO_ID NO_ID} is returned.</li>
* </ul>
*/
public final long getId() {
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/RowPresenter.java b/leanback/leanback/src/main/java/androidx/leanback/widget/RowPresenter.java
index af7e50e..6433695 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/RowPresenter.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/RowPresenter.java
@@ -467,10 +467,12 @@
/**
* Sets the policy of updating row view activated status. Can be one of:
- * <li> Default value {@link #SYNC_ACTIVATED_TO_EXPANDED}
- * <li> {@link #SYNC_ACTIVATED_TO_SELECTED}
- * <li> {@link #SYNC_ACTIVATED_TO_EXPANDED_AND_SELECTED}
- * <li> {@link #SYNC_ACTIVATED_CUSTOM}
+ * <ul>
+ * <li> Default value {@link #SYNC_ACTIVATED_TO_EXPANDED}</li>
+ * <li> {@link #SYNC_ACTIVATED_TO_SELECTED}</li>
+ * <li> {@link #SYNC_ACTIVATED_TO_EXPANDED_AND_SELECTED}</li>
+ * <li> {@link #SYNC_ACTIVATED_CUSTOM}</li>
+ * </ul>
*/
public final void setSyncActivatePolicy(int syncActivatePolicy) {
mSyncActivatePolicy = syncActivatePolicy;
@@ -478,10 +480,12 @@
/**
* Returns the policy of updating row view activated status. Can be one of:
- * <li> Default value {@link #SYNC_ACTIVATED_TO_EXPANDED}
- * <li> {@link #SYNC_ACTIVATED_TO_SELECTED}
- * <li> {@link #SYNC_ACTIVATED_TO_EXPANDED_AND_SELECTED}
- * <li> {@link #SYNC_ACTIVATED_CUSTOM}
+ * <ul>
+ * <li> Default value {@link #SYNC_ACTIVATED_TO_EXPANDED}</li>
+ * <li> {@link #SYNC_ACTIVATED_TO_SELECTED}</li>
+ * <li> {@link #SYNC_ACTIVATED_TO_EXPANDED_AND_SELECTED}</li>
+ * <li> {@link #SYNC_ACTIVATED_CUSTOM}</li>
+ * </ul>
*/
public final int getSyncActivatePolicy() {
return mSyncActivatePolicy;
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/ShadowOverlayHelper.java b/leanback/leanback/src/main/java/androidx/leanback/widget/ShadowOverlayHelper.java
index 1dfd653..63a6da7 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/ShadowOverlayHelper.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/ShadowOverlayHelper.java
@@ -30,13 +30,15 @@
* Initialize it with ShadowOverlayHelper.Builder and it decides the best strategy based
* on options user choose and current platform version.
*
+ * <ul>
* <li> For shadow: it may use 9-patch with opticalBounds or Z-value based shadow for
* API >= 21. When 9-patch is used, it requires a ShadowOverlayContainer
- * to include 9-patch views.
+ * to include 9-patch views.</li>
* <li> For overlay: it may use ShadowOverlayContainer which overrides draw() or it may
* use setForeground(new ColorDrawable()) for API>=23. The foreground support
- * might be disabled if rounded corner is applied due to performance reason.
- * <li> For rounded-corner: it uses a ViewOutlineProvider for API>=21.
+ * might be disabled if rounded corner is applied due to performance reason.</li>
+ * <li> For rounded-corner: it uses a ViewOutlineProvider for API>=21.</li>
+ * </ul>
*
* There are two different strategies: use Wrapper with a ShadowOverlayContainer;
* or apply rounded corner, overlay and rounded-corner to the view itself. Below is an example
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/picker/DatePicker.java b/leanback/leanback/src/main/java/androidx/leanback/widget/picker/DatePicker.java
index 7ce9b57..c9cc934 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/picker/DatePicker.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/picker/DatePicker.java
@@ -303,16 +303,16 @@
}
@Override
- public final void onColumnValueChanged(int column, int newVal) {
+ public final void onColumnValueChanged(int columnIndex, int newValue) {
mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis());
// take care of wrapping of days and months to update greater fields
- int oldVal = getColumnAt(column).getCurrentValue();
- if (column == mColDayIndex) {
- mTempDate.add(Calendar.DAY_OF_MONTH, newVal - oldVal);
- } else if (column == mColMonthIndex) {
- mTempDate.add(Calendar.MONTH, newVal - oldVal);
- } else if (column == mColYearIndex) {
- mTempDate.add(Calendar.YEAR, newVal - oldVal);
+ int oldVal = getColumnAt(columnIndex).getCurrentValue();
+ if (columnIndex == mColDayIndex) {
+ mTempDate.add(Calendar.DAY_OF_MONTH, newValue - oldVal);
+ } else if (columnIndex == mColMonthIndex) {
+ mTempDate.add(Calendar.MONTH, newValue - oldVal);
+ } else if (columnIndex == mColYearIndex) {
+ mTempDate.add(Calendar.YEAR, newValue - oldVal);
} else {
throw new IllegalArgumentException();
}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/picker/Picker.java b/leanback/leanback/src/main/java/androidx/leanback/widget/picker/Picker.java
index bb159d9..f037729 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/picker/Picker.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/picker/Picker.java
@@ -48,14 +48,16 @@
* the current value of PickerColumn.
* <p>
* Picker has two states and will change height:
+ * <ul>
* <li>{@link #isActivated()} is true: Picker shows typically three items vertically (see
* {@link #getActivatedVisibleItemCount()}}. Columns other than {@link #getSelectedColumn()} still
* shows one item if the Picker is focused. On a touch screen device, the Picker will not get focus
* so it always show three items on all columns. On a non-touch device (a TV), the Picker will show
* three items only on currently activated column. If the Picker has focus, it will intercept DPAD
- * directions and select activated column.
+ * directions and select activated column.</li>
* <li>{@link #isActivated()} is false: Picker shows one item vertically (see
- * {@link #getVisibleItemCount()}) on all columns. The size of Picker shrinks.
+ * {@link #getVisibleItemCount()}) on all columns. The size of Picker shrinks.</li>
+ * </ul>
*/
public class Picker extends FrameLayout {
diff --git a/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/ApiLintVersionsTest.kt b/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/ApiLintVersionsTest.kt
index 07406d5..a917fde5 100644
--- a/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/ApiLintVersionsTest.kt
+++ b/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/ApiLintVersionsTest.kt
@@ -35,6 +35,6 @@
Assert.assertEquals(CURRENT_API, registry.api)
// Intentionally fails in IDE, because we use different API version in
// studio and command line
- Assert.assertEquals(3, registry.minApi)
+ Assert.assertEquals(8, registry.minApi)
}
}
diff --git a/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/LifecycleWhenChecksTest.kt b/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/LifecycleWhenChecksTest.kt
index ad180a1..a8ad935 100644
--- a/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/LifecycleWhenChecksTest.kt
+++ b/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/LifecycleWhenChecksTest.kt
@@ -33,6 +33,7 @@
private fun check(body: String): TestLintResult {
return TestLintTask.lint()
.files(VIEW_STUB, LIFECYCLE_STUB, COROUTINES_STUB, kt(template(body)))
+ .allowCompilationErrors(true) // b/193267317
.issues(ISSUE)
.run()
}
diff --git a/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/RepeatOnLifecycleDetectorTest.kt b/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/RepeatOnLifecycleDetectorTest.kt
index b73fbd5..e58858c 100644
--- a/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/RepeatOnLifecycleDetectorTest.kt
+++ b/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/RepeatOnLifecycleDetectorTest.kt
@@ -106,6 +106,7 @@
*REPEAT_ON_LIFECYCLE_STUBS,
TestFiles.kt(fileToAdd)
)
+ .allowCompilationErrors(true) // b/193267317
.issues(RepeatOnLifecycleDetector.ISSUE)
.run()
}
diff --git a/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/WhenMethodsTest.kt b/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/WhenMethodsTest.kt
index 895536b..3d16dd3 100644
--- a/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/WhenMethodsTest.kt
+++ b/lifecycle/lifecycle-runtime-ktx-lint/src/test/java/androidx/lifecycle/lint/WhenMethodsTest.kt
@@ -49,6 +49,7 @@
private fun check(body: String): TestLintResult {
return TestLintTask.lint()
+ .allowCompilationErrors(true) // b/193267317
.files(VIEW_STUB, LIFECYCLE_STUB, COROUTINES_STUB, TestFiles.kt(template(body)))
.issues(LifecycleWhenChecks.ISSUE)
.run()
diff --git a/lint-checks/src/test/java/androidx/build/lint/BanUncheckedReflectionTest.kt b/lint-checks/src/test/java/androidx/build/lint/BanUncheckedReflectionTest.kt
index 6c66b16..a2ab068 100644
--- a/lint-checks/src/test/java/androidx/build/lint/BanUncheckedReflectionTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/BanUncheckedReflectionTest.kt
@@ -18,6 +18,7 @@
package androidx.build.lint
+import androidx.build.lint.Stubs.Companion.RestrictTo
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -33,6 +34,7 @@
fun `Detection of unchecked reflection in real-world Java sources`() {
val input = arrayOf(
javaSample("androidx.sample.core.app.ActivityRecreator"),
+ RestrictTo
)
/* ktlint-disable max-line-length */
@@ -57,6 +59,7 @@
fun `Checked reflection in real-world Java sources`() {
val input = arrayOf(
javaSample("androidx.sample.core.app.ActivityRecreatorChecked"),
+ RestrictTo
)
/* ktlint-disable max-line-length */
diff --git a/lint-checks/src/test/java/androidx/build/lint/ClassVerificationFailureDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/ClassVerificationFailureDetectorTest.kt
index f3dfe69..c2ebbf1 100644
--- a/lint-checks/src/test/java/androidx/build/lint/ClassVerificationFailureDetectorTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/ClassVerificationFailureDetectorTest.kt
@@ -18,6 +18,9 @@
package androidx.build.lint
+import androidx.build.lint.Stubs.Companion.RequiresApi
+import androidx.build.lint.Stubs.Companion.IntRange
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -32,10 +35,13 @@
),
) {
+ @Ignore // Started failing with AGP 7.1.0-alpha03 upgrade
@Test
fun `Detection of unsafe references in Java sources`() {
val input = arrayOf(
javaSample("androidx.ClassVerificationFailureFromJava"),
+ RequiresApi,
+ IntRange
)
/* ktlint-disable max-line-length */
@@ -257,6 +263,7 @@
fun `Auto-fix unsafe reference in Java source with existing inner class`() {
val input = arrayOf(
javaSample("androidx.AutofixUnsafeReferenceWithExistingClassJava"),
+ RequiresApi
)
/* ktlint-disable max-line-length */
diff --git a/lint-checks/src/test/java/androidx/build/lint/Stubs.kt b/lint-checks/src/test/java/androidx/build/lint/Stubs.kt
index 06a6353..4db14ac 100644
--- a/lint-checks/src/test/java/androidx/build/lint/Stubs.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/Stubs.kt
@@ -159,6 +159,92 @@
"""
)
+ val RequiresApi = TestFiles.java(
+ """
+package androidx.annotation;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PACKAGE;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Documented
+@Retention(CLASS)
+@Target({TYPE, METHOD, CONSTRUCTOR, FIELD, PACKAGE})
+public @interface RequiresApi {
+ @IntRange(from = 1)
+ int value() default 1;
+ @IntRange(from = 1)
+ int api() default 1;
+}
+ """
+ )
+
+ val IntRange = TestFiles.java(
+ """
+package androidx.annotation;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Documented
+@Retention(CLASS)
+@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE})
+public @interface IntRange {
+ long from() default Long.MIN_VALUE;
+ long to() default Long.MAX_VALUE;
+}
+ """
+ )
+
+ val RestrictTo = TestFiles.java(
+ """
+package androidx.annotation;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PACKAGE;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Documented
+@Retention(CLASS)
+@Target({ANNOTATION_TYPE, TYPE, METHOD, CONSTRUCTOR, FIELD, PACKAGE})
+public @interface RestrictTo {
+ Scope[] value();
+ enum Scope {
+ LIBRARY,
+ LIBRARY_GROUP,
+ LIBRARY_GROUP_PREFIX,
+ @Deprecated
+ GROUP_ID,
+ TESTS,
+ SUBCLASSES,
+ }
+}
+ """
+ )
+
/* ktlint-enable max-line-length */
}
}
diff --git a/media/media/build.gradle b/media/media/build.gradle
index f2067b9..e77d96e 100644
--- a/media/media/build.gradle
+++ b/media/media/build.gradle
@@ -24,7 +24,7 @@
}
dependencies {
- api("androidx.core:core:1.6.0-rc01")
+ api("androidx.core:core:1.6.0")
implementation("androidx.annotation:annotation:1.2.0")
implementation("androidx.collection:collection:1.1.0")
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt
index 24097d0..0de4fd2 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt
@@ -196,6 +196,26 @@
savedStateRegistryController.performSave(outBundle)
}
+ override fun equals(other: Any?): Boolean {
+ if (other == null || other !is NavBackStackEntry) return false
+ return context == other.context && id == other.id && destination == other.destination &&
+ (
+ arguments == other.arguments ||
+ arguments?.keySet()
+ ?.all { arguments!!.get(it) == other.arguments?.get(it) } == true
+ )
+ }
+
+ override fun hashCode(): Int {
+ var result = context.hashCode()
+ result = 31 * result + id.hashCode()
+ result = 31 * result + destination.hashCode()
+ arguments?.keySet()?.forEach {
+ result = 31 * result + arguments!!.get(it).hashCode()
+ }
+ return result
+ }
+
/**
* Used to create the {SavedStateViewModel}
*/
diff --git a/navigation/navigation-compose/api/current.txt b/navigation/navigation-compose/api/current.txt
index 89e6e04..ecad6d42 100644
--- a/navigation/navigation-compose/api/current.txt
+++ b/navigation/navigation-compose/api/current.txt
@@ -10,6 +10,10 @@
ctor public ComposeNavigator.Destination(androidx.navigation.compose.ComposeNavigator navigator, kotlin.jvm.functions.Function1<? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
}
+ public final class DialogHostKt {
+ method @androidx.compose.runtime.Composable public static void DialogHost(androidx.navigation.compose.DialogNavigator dialogNavigator);
+ }
+
@androidx.navigation.Navigator.Name("dialog") public final class DialogNavigator extends androidx.navigation.Navigator<androidx.navigation.compose.DialogNavigator.Destination> {
ctor public DialogNavigator();
method public androidx.navigation.compose.DialogNavigator.Destination createDestination();
diff --git a/navigation/navigation-compose/api/public_plus_experimental_current.txt b/navigation/navigation-compose/api/public_plus_experimental_current.txt
index 89e6e04..ecad6d42 100644
--- a/navigation/navigation-compose/api/public_plus_experimental_current.txt
+++ b/navigation/navigation-compose/api/public_plus_experimental_current.txt
@@ -10,6 +10,10 @@
ctor public ComposeNavigator.Destination(androidx.navigation.compose.ComposeNavigator navigator, kotlin.jvm.functions.Function1<? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
}
+ public final class DialogHostKt {
+ method @androidx.compose.runtime.Composable public static void DialogHost(androidx.navigation.compose.DialogNavigator dialogNavigator);
+ }
+
@androidx.navigation.Navigator.Name("dialog") public final class DialogNavigator extends androidx.navigation.Navigator<androidx.navigation.compose.DialogNavigator.Destination> {
ctor public DialogNavigator();
method public androidx.navigation.compose.DialogNavigator.Destination createDestination();
diff --git a/navigation/navigation-compose/api/restricted_current.txt b/navigation/navigation-compose/api/restricted_current.txt
index 89e6e04..ecad6d42 100644
--- a/navigation/navigation-compose/api/restricted_current.txt
+++ b/navigation/navigation-compose/api/restricted_current.txt
@@ -10,6 +10,10 @@
ctor public ComposeNavigator.Destination(androidx.navigation.compose.ComposeNavigator navigator, kotlin.jvm.functions.Function1<? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
}
+ public final class DialogHostKt {
+ method @androidx.compose.runtime.Composable public static void DialogHost(androidx.navigation.compose.DialogNavigator dialogNavigator);
+ }
+
@androidx.navigation.Navigator.Name("dialog") public final class DialogNavigator extends androidx.navigation.Navigator<androidx.navigation.compose.DialogNavigator.Destination> {
ctor public DialogNavigator();
method public androidx.navigation.compose.DialogNavigator.Destination createDestination();
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index f9eda4f..2a687f8 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -31,6 +31,7 @@
implementation(libs.kotlinStdlib)
implementation("androidx.compose.foundation:foundation-layout:1.0.0-rc01")
api("androidx.activity:activity-compose:1.3.0-rc01")
+ api("androidx.compose.animation:animation:1.0.0-rc01")
api("androidx.compose.runtime:runtime:1.0.0-rc01")
api("androidx.compose.runtime:runtime-saveable:1.0.0-rc01")
api("androidx.compose.ui:ui:1.0.0-rc01")
diff --git a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/DialogNavigatorTest.kt b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/DialogNavigatorTest.kt
index eb6ffb4..98b57cb 100644
--- a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/DialogNavigatorTest.kt
+++ b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/DialogNavigatorTest.kt
@@ -42,7 +42,7 @@
navigator.onAttach(navigatorState)
rule.setContent {
- navigator.Dialogs()
+ DialogHost(navigator)
}
rule.onNodeWithText(defaultText).assertDoesNotExist()
@@ -68,7 +68,7 @@
navigator.navigate(listOf(entry), null, null)
rule.setContent {
- navigator.Dialogs()
+ DialogHost(navigator)
}
rule.onNodeWithText(defaultText).assertIsDisplayed()
diff --git a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
index 4b29e83..26b0739 100644
--- a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
+++ b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
@@ -19,8 +19,10 @@
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
+import androidx.compose.animation.core.AnimationConstants.DefaultDurationMillis
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.text.BasicText
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.MutableState
@@ -35,6 +37,7 @@
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
+import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
@@ -382,6 +385,89 @@
}
}
+ @Test
+ fun testNavHostCrossFade() {
+ lateinit var navController: NavHostController
+ composeTestRule.setContent {
+ navController = rememberNavController()
+ NavHost(navController, startDestination = first) {
+ composable(first) { BasicText(first) }
+ composable(second) { BasicText(second) }
+ }
+ }
+
+ val firstEntry = navController.currentBackStackEntry
+
+ composeTestRule.runOnIdle {
+ assertThat(firstEntry?.lifecycle?.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ }
+
+ composeTestRule.mainClock.autoAdvance = false
+
+ composeTestRule.runOnIdle {
+ navController.navigate(second)
+ }
+
+ assertThat(firstEntry?.lifecycle?.currentState)
+ .isEqualTo(Lifecycle.State.CREATED)
+ assertThat(navController.currentBackStackEntry?.lifecycle?.currentState)
+ .isEqualTo(Lifecycle.State.STARTED)
+
+ // advance half way between the crossfade
+ composeTestRule.mainClock.advanceTimeBy(DefaultDurationMillis.toLong() / 2)
+
+ assertThat(firstEntry?.lifecycle?.currentState)
+ .isEqualTo(Lifecycle.State.CREATED)
+ assertThat(navController.currentBackStackEntry?.lifecycle?.currentState)
+ .isEqualTo(Lifecycle.State.STARTED)
+
+ composeTestRule.onNodeWithText(first).assertExists()
+ composeTestRule.onNodeWithText(second).assertExists()
+
+ composeTestRule.mainClock.autoAdvance = true
+
+ composeTestRule.runOnIdle {
+ assertThat(firstEntry?.lifecycle?.currentState)
+ .isEqualTo(Lifecycle.State.CREATED)
+ assertThat(navController.currentBackStackEntry?.lifecycle?.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ }
+
+ composeTestRule.mainClock.autoAdvance = false
+
+ val secondEntry = navController.currentBackStackEntry
+
+ composeTestRule.runOnIdle {
+ navController.popBackStack()
+ }
+
+ assertThat(navController.currentBackStackEntry?.lifecycle?.currentState)
+ .isEqualTo(Lifecycle.State.STARTED)
+ assertThat(secondEntry?.lifecycle?.currentState)
+ .isEqualTo(Lifecycle.State.CREATED)
+
+ // advance half way between the crossfade
+ composeTestRule.mainClock.advanceTimeBy(DefaultDurationMillis.toLong() / 2)
+
+ assertThat(navController.currentBackStackEntry?.lifecycle?.currentState)
+ .isEqualTo(Lifecycle.State.STARTED)
+ assertThat(secondEntry?.lifecycle?.currentState)
+ .isEqualTo(Lifecycle.State.CREATED)
+
+ composeTestRule.onNodeWithText(first).assertExists()
+ composeTestRule.onNodeWithText(second).assertExists()
+
+ composeTestRule.mainClock.autoAdvance = true
+
+ composeTestRule.runOnIdle {
+ assertThat(navController.currentBackStackEntry?.lifecycle?.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ assertThat(secondEntry?.lifecycle?.currentState)
+ .isEqualTo(Lifecycle.State.DESTROYED)
+ }
+ }
+
private fun createNavController(context: Context): TestNavHostController {
val navController = TestNavHostController(context)
val navigator = TestNavigator()
@@ -390,6 +476,9 @@
}
}
+private const val first = "first"
+private const val second = "second"
+
class TestViewModel : ViewModel() {
var value: String = "nothing"
}
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/ComposeNavigator.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/ComposeNavigator.kt
index e44a9a6..01bfa71 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/ComposeNavigator.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/ComposeNavigator.kt
@@ -25,6 +25,7 @@
import androidx.navigation.NavOptions
import androidx.navigation.Navigator
import androidx.navigation.NavigatorState
+import androidx.navigation.NavigatorState.OnTransitionCompleteListener
import androidx.navigation.compose.ComposeNavigator.Destination
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -38,6 +39,13 @@
public class ComposeNavigator : Navigator<Destination>() {
private var attached by mutableStateOf(false)
+ internal val transitionsInProgress:
+ StateFlow<Map<NavBackStackEntry, OnTransitionCompleteListener>> get() = if (attached) {
+ state.transitionsInProgress
+ } else {
+ MutableStateFlow(emptyMap())
+ }
+
/**
* Get the back stack from the [state]. NavHost will compose at least
* once (due to the use of [androidx.compose.runtime.DisposableEffect]) before
@@ -61,7 +69,7 @@
navigatorExtras: Extras?
) {
entries.forEach { entry ->
- state.push(entry)
+ state.pushWithTransition(entry)
}
}
@@ -70,7 +78,7 @@
}
override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
- state.pop(popUpTo, savedState)
+ state.popWithTransition(popUpTo, savedState)
}
/**
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogHost.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogHost.kt
new file mode 100644
index 0000000..c8ef92c
--- /dev/null
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogHost.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.navigation.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.saveable.rememberSaveableStateHolder
+import androidx.compose.ui.window.Dialog
+import androidx.lifecycle.Lifecycle
+import androidx.navigation.compose.DialogNavigator.Destination
+
+/**
+ * Show each [Destination] on the [DialogNavigator]'s back stack as a [Dialog].
+ *
+ * Note that [NavHost] will call this for you; you do not need to call it manually.
+ */
+@Composable
+public fun DialogHost(dialogNavigator: DialogNavigator) {
+ val saveableStateHolder = rememberSaveableStateHolder()
+ val dialogBackStack by dialogNavigator.backStack.collectAsState()
+
+ dialogBackStack.filter { backStackEntry ->
+ backStackEntry.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
+ }.forEach { backStackEntry ->
+ val destination = backStackEntry.destination as Destination
+ Dialog(
+ onDismissRequest = { dialogNavigator.dismiss(backStackEntry) },
+ properties = destination.dialogProperties
+ ) {
+ // while in the scope of the composable, we provide the navBackStackEntry as the
+ // ViewModelStoreOwner and LifecycleOwner
+ backStackEntry.LocalOwnersProvider(saveableStateHolder) {
+ destination.content(backStackEntry)
+ }
+ }
+ }
+}
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogNavigator.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogNavigator.kt
index a882259..e23b3a8 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogNavigator.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/DialogNavigator.kt
@@ -17,14 +17,11 @@
package androidx.navigation.compose
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
-import androidx.lifecycle.Lifecycle
import androidx.navigation.FloatingWindow
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDestination
@@ -50,36 +47,20 @@
* the Navigator is attached, so we specifically return an empty flow if we
* aren't attached yet.
*/
- private val backStack: StateFlow<List<NavBackStackEntry>> get() = if (attached) {
+ internal val backStack: StateFlow<List<NavBackStackEntry>> get() = if (attached) {
state.backStack
} else {
MutableStateFlow(emptyList())
}
/**
- * Show each [Destination] on the back stack as a [Dialog].
- *
- * Note that [NavHost] will call this for you; you do not need to call it manually.
+ * Dismiss the dialog destination associated with the given [backStackEntry].
*/
- internal val Dialogs: @Composable () -> Unit = @Composable {
- val saveableStateHolder = rememberSaveableStateHolder()
- val dialogBackStack by backStack.collectAsState()
-
- dialogBackStack.filter { backStackEntry ->
- backStackEntry.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
- }.forEach { backStackEntry ->
- val destination = backStackEntry.destination as Destination
- Dialog(
- onDismissRequest = { state.pop(backStackEntry, false) },
- properties = destination.dialogProperties
- ) {
- // while in the scope of the composable, we provide the navBackStackEntry as the
- // ViewModelStoreOwner and LifecycleOwner
- backStackEntry.LocalOwnersProvider(saveableStateHolder) {
- destination.content(backStackEntry)
- }
- }
+ internal fun dismiss(backStackEntry: NavBackStackEntry) {
+ check(attached) {
+ "The DialogNavigator must be attached to a NavController to call dismiss"
}
+ state.pop(backStackEntry, false)
}
override fun onAttach(state: NavigatorState) {
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
index 1f609a8..b0ede34 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
@@ -17,9 +17,10 @@
package androidx.navigation.compose
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
-import androidx.compose.foundation.layout.Box
+import androidx.compose.animation.Crossfade
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@@ -122,16 +123,38 @@
ComposeNavigator.NAME
) as? ComposeNavigator ?: return
val backStack by composeNavigator.backStack.collectAsState()
+ val transitionsInProgress by composeNavigator.transitionsInProgress.collectAsState()
- backStack.filter { backStackEntry ->
- backStackEntry.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
- }.forEach { backStackEntry ->
- val destination = backStackEntry.destination as ComposeNavigator.Destination
+ val backStackEntry = transitionsInProgress.keys.lastOrNull { entry ->
+ entry.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
+ } ?: backStack.lastOrNull { entry ->
+ entry.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
+ }
+
+ SideEffect {
+ // When we place the first entry on the backstack we won't get a call on onDispose since
+ // the Crossfade will remain in the compose hierarchy. We need to move that entry to
+ // RESUMED separately.
+ if (backStack.size == 1 && transitionsInProgress.size == 1) {
+ transitionsInProgress.forEach { entry ->
+ entry.value.onTransitionComplete()
+ }
+ }
+ }
+
+ if (backStackEntry != null) {
// while in the scope of the composable, we provide the navBackStackEntry as the
// ViewModelStoreOwner and LifecycleOwner
- Box(modifier, propagateMinConstraints = true) {
- backStackEntry.LocalOwnersProvider(saveableStateHolder) {
- destination.content(backStackEntry)
+ Crossfade(backStackEntry, modifier) { currentEntry ->
+ currentEntry.LocalOwnersProvider(saveableStateHolder) {
+ (currentEntry.destination as ComposeNavigator.Destination).content(currentEntry)
+ }
+ DisposableEffect(currentEntry) {
+ onDispose {
+ transitionsInProgress.forEach { entry ->
+ entry.value.onTransitionComplete()
+ }
+ }
}
}
}
@@ -141,5 +164,5 @@
) as? DialogNavigator ?: return
// Show any dialog destinations
- dialogNavigator.Dialogs()
+ DialogHost(dialogNavigator)
}
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/build.gradle b/navigation/navigation-fragment/build.gradle
index 8d49434..1b90feb 100644
--- a/navigation/navigation-fragment/build.gradle
+++ b/navigation/navigation-fragment/build.gradle
@@ -26,7 +26,7 @@
dependencies {
api(projectOrArtifact(":fragment:fragment-ktx"))
api(project(":navigation:navigation-runtime"))
- api(projectOrArtifact(":slidingpanelayout:slidingpanelayout"))
+ api("androidx.slidingpanelayout:slidingpanelayout:1.2.0-alpha03")
api(libs.kotlinStdlib)
androidTestImplementation(project(":navigation:navigation-testing"))
androidTestImplementation(projectOrArtifact(":fragment:fragment-testing"))
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/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
index 99e346f..536dd8e 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -280,11 +280,21 @@
popUpTo: NavBackStackEntry,
saveState: Boolean
): OnTransitionCompleteListener {
+ // we need to mark the entry as transitioning before making the super call to pop so
+ // we don't move its lifecycle to DESTROYED.
+ addInProgressTransition(popUpTo) { }
val innerListener = super.popWithTransition(popUpTo, saveState)
val listener = OnTransitionCompleteListener {
innerListener.onTransitionComplete()
if (backQueue.contains(popUpTo)) {
updateBackStackLifecycle()
+ } else {
+ // If the entry is no longer part of the backStack, we need to manually move
+ // it to DESTROYED, and clear its view model
+ popUpTo.maxLifecycle = Lifecycle.State.DESTROYED
+ if (!saveState) {
+ viewModel?.clear(popUpTo.id)
+ }
}
}
addInProgressTransition(popUpTo, listener)
@@ -588,6 +598,16 @@
.getNavigator<Navigator<NavDestination>>(entry.destination.navigatorName)
val state = navigatorState[navigator]
val transitioning = state?.transitionsInProgress?.value?.containsKey(entry)
+ // When popping, we need to mark the incoming entry as transitioning so we keep it
+ // STARTED until the transition completes at which point we can move it to RESUMED
+ if (backQueue.isNotEmpty() && transitioning == true) {
+ state.addInProgressTransition(backQueue.last()) {
+ state.removeInProgressTransition(backQueue.last())
+ if (!state.isNavigating) {
+ updateBackStackLifecycle()
+ }
+ }
+ }
if (entry.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
if (saveState) {
// Move the state through STOPPED
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/DataSource.kt b/paging/common/src/main/kotlin/androidx/paging/DataSource.kt
index 76ad766..677c811 100644
--- a/paging/common/src/main/kotlin/androidx/paging/DataSource.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/DataSource.kt
@@ -23,8 +23,6 @@
import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
-import java.util.concurrent.CopyOnWriteArrayList
-import java.util.concurrent.atomic.AtomicBoolean
/**
* Base class for loading pages of snapshot data into a [PagedList].
@@ -101,17 +99,20 @@
// subclassing, except through exposed subclasses.
internal constructor(internal val type: KeyType) {
- @VisibleForTesting
- internal val onInvalidatedCallbacks = CopyOnWriteArrayList<InvalidatedCallback>()
+ private val invalidateCallbackTracker = InvalidateCallbackTracker<InvalidatedCallback> {
+ it.onInvalidated()
+ }
- private val _invalid = AtomicBoolean(false)
+ internal val invalidateCallbackCount: Int
+ @VisibleForTesting
+ get() = invalidateCallbackTracker.callbackCount()
/**
* @return `true` if the data source is invalid, and can no longer be queried for data.
*/
public open val isInvalid: Boolean
@WorkerThread
- get() = _invalid.get()
+ get() = invalidateCallbackTracker.invalid
/**
* Factory for DataSources.
@@ -360,13 +361,16 @@
* A data source will only invoke its callbacks once - the first time [invalidate] is called, on
* that thread.
*
+ * If this [DataSource] is already invalid, the provided [onInvalidatedCallback] will be
+ * triggered immediately.
+ *
* @param onInvalidatedCallback The callback, will be invoked on thread that invalidates the
* [DataSource].
*/
@AnyThread
@Suppress("RegistrationName")
public open fun addInvalidatedCallback(onInvalidatedCallback: InvalidatedCallback) {
- onInvalidatedCallbacks.add(onInvalidatedCallback)
+ invalidateCallbackTracker.registerInvalidatedCallback(onInvalidatedCallback)
}
/**
@@ -377,7 +381,7 @@
@AnyThread
@Suppress("RegistrationName")
public open fun removeInvalidatedCallback(onInvalidatedCallback: InvalidatedCallback) {
- onInvalidatedCallbacks.remove(onInvalidatedCallback)
+ invalidateCallbackTracker.unregisterInvalidatedCallback(onInvalidatedCallback)
}
/**
@@ -387,9 +391,7 @@
*/
@AnyThread
public open fun invalidate() {
- if (_invalid.compareAndSet(false, true)) {
- onInvalidatedCallbacks.forEach { it.onInvalidated() }
- }
+ invalidateCallbackTracker.invalidate()
}
/**
diff --git a/paging/common/src/main/kotlin/androidx/paging/InvalidateCallbackTracker.kt b/paging/common/src/main/kotlin/androidx/paging/InvalidateCallbackTracker.kt
new file mode 100644
index 0000000..88afc1d
--- /dev/null
+++ b/paging/common/src/main/kotlin/androidx/paging/InvalidateCallbackTracker.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.paging
+
+import androidx.annotation.VisibleForTesting
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/**
+ * Helper class for thread-safe invalidation callback tracking + triggering on registration.
+ */
+internal class InvalidateCallbackTracker<T>(
+ private val callbackInvoker: (T) -> Unit
+) {
+ private val lock = ReentrantLock()
+ private val callbacks = mutableListOf<T>()
+ internal var invalid = false
+ private set
+
+ @VisibleForTesting
+ internal fun callbackCount() = callbacks.size
+
+ internal fun registerInvalidatedCallback(callback: T) {
+ if (invalid) {
+ callbackInvoker(callback)
+ return
+ }
+
+ var callImmediately = false
+ lock.withLock {
+ if (invalid) {
+ callImmediately = true
+ } else {
+ callbacks.add(callback)
+ }
+ }
+
+ if (callImmediately) {
+ callbackInvoker(callback)
+ }
+ }
+
+ internal fun unregisterInvalidatedCallback(callback: T) {
+ lock.withLock {
+ callbacks.remove(callback)
+ }
+ }
+
+ internal fun invalidate() {
+ if (invalid) return
+
+ var callbacksToInvoke: List<T>?
+ lock.withLock {
+ if (invalid) return
+
+ invalid = true
+ callbacksToInvoke = callbacks.toList()
+ callbacks.clear()
+ }
+
+ callbacksToInvoke?.forEach(callbackInvoker)
+ }
+}
\ No newline at end of file
diff --git a/paging/common/src/main/kotlin/androidx/paging/LegacyPageFetcher.kt b/paging/common/src/main/kotlin/androidx/paging/LegacyPageFetcher.kt
index 1bc1f44..868af5e 100644
--- a/paging/common/src/main/kotlin/androidx/paging/LegacyPageFetcher.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/LegacyPageFetcher.kt
@@ -63,6 +63,7 @@
when (value) {
is PagingSource.LoadResult.Page -> onLoadSuccess(type, value)
is PagingSource.LoadResult.Error -> onLoadError(type, value.throwable)
+ is PagingSource.LoadResult.Invalid -> onLoadInvalid()
}
}
}
@@ -92,6 +93,11 @@
loadStateManager.setState(type, state)
}
+ private fun onLoadInvalid() {
+ source.invalidate()
+ detach()
+ }
+
fun trySchedulePrepend() {
val startState = loadStateManager.startState
if (startState is NotLoading && !startState.endOfPaginationReached) {
diff --git a/paging/common/src/main/kotlin/androidx/paging/PageFetcher.kt b/paging/common/src/main/kotlin/androidx/paging/PageFetcher.kt
index b04d64a..cd03e7f 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PageFetcher.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PageFetcher.kt
@@ -60,12 +60,9 @@
}
.simpleScan(null) { previousGeneration: GenerationInfo<Key, Value>?,
triggerRemoteRefresh: Boolean ->
- var pagingSource = generateNewPagingSource(
+ val pagingSource = generateNewPagingSource(
previousPagingSource = previousGeneration?.snapshot?.pagingSource
)
- while (pagingSource.invalid) {
- pagingSource = generateNewPagingSource(previousPagingSource = pagingSource)
- }
var previousPagingState = previousGeneration?.snapshot?.currentPagingState()
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..08bdd31 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PagingSource.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PagingSource.kt
@@ -20,8 +20,6 @@
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.paging.LoadType.REFRESH
-import java.util.concurrent.CopyOnWriteArrayList
-import java.util.concurrent.atomic.AtomicBoolean
/** @suppress */
@Suppress("DEPRECATION")
@@ -82,6 +80,14 @@
*/
public abstract class PagingSource<Key : Any, Value : Any> {
+ private val invalidateCallbackTracker = InvalidateCallbackTracker<() -> Unit>() {
+ it()
+ }
+
+ internal val invalidateCallbackCount: Int
+ @VisibleForTesting
+ get() = invalidateCallbackTracker.callbackCount()
+
/**
* Params for a load request on a [PagingSource] from [PagingSource.load].
*/
@@ -204,6 +210,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
@@ -295,17 +319,12 @@
public open val keyReuseSupported: Boolean
get() = false
- @VisibleForTesting
- internal val onInvalidatedCallbacks = CopyOnWriteArrayList<() -> Unit>()
-
- private val _invalid = AtomicBoolean(false)
-
/**
* Whether this [PagingSource] has been invalidated, which should happen when the data this
* [PagingSource] represents changes since it was first instantiated.
*/
public val invalid: Boolean
- get() = _invalid.get()
+ get() = invalidateCallbackTracker.invalid
/**
* Signal the [PagingSource] to stop loading.
@@ -314,9 +333,7 @@
* this method should have no effect.
*/
public fun invalidate() {
- if (_invalid.compareAndSet(false, true)) {
- onInvalidatedCallbacks.forEach { it.invoke() }
- }
+ invalidateCallbackTracker.invalidate()
}
/**
@@ -327,11 +344,14 @@
* A [PagingSource] will only invoke its callbacks once - the first time [invalidate] is called,
* on that thread.
*
+ * If this [PagingSource] is already invalid, the provided [onInvalidatedCallback] will be
+ * triggered immediately.
+ *
* @param onInvalidatedCallback The callback that will be invoked on thread that invalidates the
* [PagingSource].
*/
public fun registerInvalidatedCallback(onInvalidatedCallback: () -> Unit) {
- onInvalidatedCallbacks.add(onInvalidatedCallback)
+ invalidateCallbackTracker.registerInvalidatedCallback(onInvalidatedCallback)
}
/**
@@ -340,7 +360,7 @@
* @param onInvalidatedCallback The previously added callback.
*/
public fun unregisterInvalidatedCallback(onInvalidatedCallback: () -> Unit) {
- onInvalidatedCallbacks.remove(onInvalidatedCallback)
+ invalidateCallbackTracker.unregisterInvalidatedCallback(onInvalidatedCallback)
}
/**
diff --git a/paging/common/src/test/kotlin/androidx/paging/ContiguousPagedListTest.kt b/paging/common/src/test/kotlin/androidx/paging/ContiguousPagedListTest.kt
index 03811c2..ffdfee5 100644
--- a/paging/common/src/test/kotlin/androidx/paging/ContiguousPagedListTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/ContiguousPagedListTest.kt
@@ -64,6 +64,8 @@
private inner class TestPagingSource(
val listData: List<Item> = ITEMS
) : PagingSource<Int, Item>() {
+ var invalidData = false
+
override fun getRefreshKey(state: PagingState<Int, Item>): Int? {
return state.anchorPosition
?.let { anchorPosition -> state.closestItemToPosition(anchorPosition)?.pos }
@@ -89,6 +91,10 @@
val start = maxOf(initPos - params.loadSize / 2, 0)
val result = getClampedRange(start, start + params.loadSize)
+ if (invalidData) {
+ invalidData = false
+ return LoadResult.Invalid()
+ }
return when {
result == null -> LoadResult.Error(EXCEPTION)
placeholdersEnabled -> Page(
@@ -109,6 +115,10 @@
private fun loadAfter(params: LoadParams<Int>): LoadResult<Int, Item> {
val result = getClampedRange(params.key!! + 1, params.key!! + 1 + params.loadSize)
?: return LoadResult.Error(EXCEPTION)
+ if (invalidData) {
+ invalidData = false
+ return LoadResult.Invalid()
+ }
return Page(
data = result,
prevKey = if (result.isNotEmpty()) result.first().pos else null,
@@ -119,6 +129,10 @@
private fun loadBefore(params: LoadParams<Int>): LoadResult<Int, Item> {
val result = getClampedRange(params.key!! - params.loadSize, params.key!!)
?: return LoadResult.Error(EXCEPTION)
+ if (invalidData) {
+ invalidData = false
+ return LoadResult.Invalid()
+ }
return Page(
data = result,
prevKey = result.firstOrNull()?.pos,
@@ -329,6 +343,30 @@
}
@Test
+ fun append_invalidData_detach() {
+ val pagedList = createCountedPagedList(0)
+ val callback = mock<Callback>()
+ pagedList.addWeakCallback(callback)
+ verifyRange(0, 40, pagedList)
+ verifyZeroInteractions(callback)
+
+ pagedList.loadAround(35)
+ // return a LoadResult.Invalid
+ val pagingSource = pagedList.pagingSource as TestPagingSource
+ pagingSource.invalidData = true
+ drain()
+
+ // nothing new should be loaded
+ verifyRange(0, 40, pagedList)
+ verifyNoMoreInteractions(callback)
+ assertTrue(pagingSource.invalid)
+ assertTrue(pagedList.isDetached)
+ // detached status should turn pagedList into immutable, and snapshot should return the
+ // pagedList itself
+ assertSame(pagedList.snapshot(), pagedList)
+ }
+
+ @Test
fun prepend() {
val pagedList = createCountedPagedList(80)
val callback = mock<Callback>()
@@ -345,6 +383,30 @@
}
@Test
+ fun prepend_invalidData_detach() {
+ val pagedList = createCountedPagedList(80)
+ val callback = mock<Callback>()
+ pagedList.addWeakCallback(callback)
+ verifyRange(60, 40, pagedList)
+ verifyZeroInteractions(callback)
+
+ pagedList.loadAround(if (placeholdersEnabled) 65 else 5)
+ // return a LoadResult.Invalid
+ val pagingSource = pagedList.pagingSource as TestPagingSource
+ pagingSource.invalidData = true
+ drain()
+
+ // nothing new should be loaded
+ verifyRange(60, 40, pagedList)
+ verifyNoMoreInteractions(callback)
+ assertTrue(pagingSource.invalid)
+ assertTrue(pagedList.isDetached)
+ // detached status should turn pagedList into immutable, and snapshot should return the
+ // pagedList itself
+ assertSame(pagedList.snapshot(), pagedList)
+ }
+
+ @Test
fun outwards() {
val pagedList = createCountedPagedList(40)
val callback = mock<Callback>()
diff --git a/paging/common/src/test/kotlin/androidx/paging/DataSourceTest.kt b/paging/common/src/test/kotlin/androidx/paging/DataSourceTest.kt
new file mode 100644
index 0000000..bd2a9fb
--- /dev/null
+++ b/paging/common/src/test/kotlin/androidx/paging/DataSourceTest.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.paging
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class DataSourceTest {
+
+ @Test
+ fun addInvalidatedCallback_triggersImmediatelyIfAlreadyInvalid() {
+ val pagingSource = TestPositionalDataSource(listOf())
+ var invalidateCalls = 0
+
+ pagingSource.invalidate()
+ pagingSource.addInvalidatedCallback { invalidateCalls++ }
+ assertThat(invalidateCalls).isEqualTo(1)
+ }
+
+ @Test
+ fun addInvalidatedCallback_avoidsRetriggeringWhenCalledRecursively() {
+ val pagingSource = TestPositionalDataSource(listOf())
+ var invalidateCalls = 0
+
+ pagingSource.addInvalidatedCallback {
+ pagingSource.addInvalidatedCallback { invalidateCalls++ }
+ pagingSource.invalidate()
+ pagingSource.addInvalidatedCallback { invalidateCalls++ }
+ invalidateCalls++
+ }
+ pagingSource.invalidate()
+ assertThat(invalidateCalls).isEqualTo(3)
+ }
+}
diff --git a/paging/common/src/test/kotlin/androidx/paging/InvalidatingPagingSourceFactoryTest.kt b/paging/common/src/test/kotlin/androidx/paging/InvalidatingPagingSourceFactoryTest.kt
index d8d9453..98388de 100644
--- a/paging/common/src/test/kotlin/androidx/paging/InvalidatingPagingSourceFactoryTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/InvalidatingPagingSourceFactoryTest.kt
@@ -73,6 +73,6 @@
testFactory.invalidate()
- assertEquals(3, invalidateCount)
+ assertEquals(4, invalidateCount)
}
}
diff --git a/paging/common/src/test/kotlin/androidx/paging/LegacyPageFetcherTest.kt b/paging/common/src/test/kotlin/androidx/paging/LegacyPageFetcherTest.kt
index 71166f4..f059a72 100644
--- a/paging/common/src/test/kotlin/androidx/paging/LegacyPageFetcherTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/LegacyPageFetcherTest.kt
@@ -27,6 +27,7 @@
import androidx.paging.PagingSource.LoadResult
import androidx.paging.PagingSource.LoadResult.Page
import androidx.testutils.TestDispatcher
+import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.runBlocking
@@ -42,6 +43,8 @@
private val data = List(9) { "$it" }
inner class ImmediateListDataSource(val data: List<String>) : PagingSource<Int, String>() {
+ var invalidData = false
+
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, String> {
val key = params.key ?: 0
@@ -52,6 +55,11 @@
}.let { (start, end) ->
start.coerceAtLeast(0) to end.coerceAtMost(data.size)
}
+
+ if (invalidData) {
+ invalidData = false
+ return LoadResult.Invalid()
+ }
return Page(
data = data.subList(start, end),
prevKey = if (start > 0) start else null,
@@ -345,4 +353,76 @@
consumer.takeStateChanges()
)
}
+
+ @Test
+ fun append_invalidData() {
+ val consumer = MockConsumer()
+ val pager = createPager(consumer, 0, 3)
+
+ // try a normal append first
+ pager.tryScheduleAppend()
+ testDispatcher.executeAll()
+
+ assertThat(consumer.takeResults()).containsExactly(
+ Result(APPEND, rangeResult(3, 5))
+ )
+ assertThat(consumer.takeStateChanges()).containsExactly(
+ StateChange(APPEND, Loading),
+ StateChange(APPEND, NotLoading.Incomplete)
+ )
+
+ // now make next append return LoadResult.Invalid
+ val pagingSource = pager.source as ImmediateListDataSource
+ pagingSource.invalidData = true
+
+ pager.tryScheduleAppend()
+ testDispatcher.executeAll()
+
+ // the load should return before returning any data
+ assertThat(consumer.takeResults()).isEmpty()
+ assertThat(consumer.takeStateChanges()).containsExactly(
+ StateChange(APPEND, Loading),
+ )
+
+ // exception handler should invalidate the paging source and result in fetcher to be
+ // detached
+ assertTrue(pagingSource.invalid)
+ assertTrue(pager.isDetached)
+ }
+
+ @Test
+ fun prepend_invalidData() {
+ val consumer = MockConsumer()
+ val pager = createPager(consumer, 6, 9)
+
+ // try a normal prepend first
+ pager.trySchedulePrepend()
+ testDispatcher.executeAll()
+
+ assertThat(consumer.takeResults()).containsExactly(
+ Result(PREPEND, rangeResult(4, 6))
+ )
+ assertThat(consumer.takeStateChanges()).containsExactly(
+ StateChange(PREPEND, Loading),
+ StateChange(PREPEND, NotLoading.Incomplete)
+ )
+
+ // now make next prepend throw error
+ val pagingSource = pager.source as ImmediateListDataSource
+ pagingSource.invalidData = true
+
+ pager.trySchedulePrepend()
+ testDispatcher.executeAll()
+
+ // the load should return before returning any data
+ assertThat(consumer.takeResults()).isEmpty()
+ assertThat(consumer.takeStateChanges()).containsExactly(
+ StateChange(PREPEND, Loading),
+ )
+
+ // exception handler should invalidate the paging source and result in fetcher to be
+ // detached
+ assertTrue(pagingSource.invalid)
+ assertTrue(pager.isDetached)
+ }
}
diff --git a/paging/common/src/test/kotlin/androidx/paging/PageFetcherSnapshotTest.kt b/paging/common/src/test/kotlin/androidx/paging/PageFetcherSnapshotTest.kt
index e1562f4..ffdac54 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PageFetcherSnapshotTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PageFetcherSnapshotTest.kt
@@ -28,6 +28,7 @@
import androidx.paging.PageEvent.Insert.Companion.Prepend
import androidx.paging.PageEvent.Insert.Companion.Refresh
import androidx.paging.PageEvent.LoadStateUpdate
+import androidx.paging.PagingSource.LoadResult
import androidx.paging.PagingSource.LoadResult.Page
import androidx.paging.RemoteMediatorMock.LoadEvent
import androidx.paging.TestPagingSource.Companion.LOAD_ERROR
@@ -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..8566009 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PageFetcherTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PageFetcherTest.kt
@@ -18,8 +18,11 @@
import androidx.paging.LoadState.Loading
import androidx.paging.LoadType.APPEND
+import androidx.paging.LoadType.PREPEND
import androidx.paging.LoadType.REFRESH
import androidx.paging.PageEvent.LoadStateUpdate
+import androidx.paging.PagingSource.LoadResult
+import androidx.paging.PagingSource.LoadResult.Page
import androidx.paging.RemoteMediator.InitializeAction.LAUNCH_INITIAL_REFRESH
import androidx.paging.RemoteMediator.InitializeAction.SKIP_INITIAL_REFRESH
import com.google.common.truth.Truth.assertThat
@@ -28,6 +31,7 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectIndexed
import kotlinx.coroutines.flow.collectLatest
@@ -40,6 +44,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import java.util.ArrayList
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNotEquals
@@ -195,6 +200,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)
@@ -292,17 +444,21 @@
// Wait for first generation to instantiate.
advanceUntilIdle()
+ // Providing an invalid PagingSource should automatically trigger invalidation
+ // regardless of when the invalidation callback is registered.
+ assertThat(pagingSources).hasSize(2)
+
// The first PagingSource is immediately invalid, so we shouldn't keep an invalidate
// listener registered on it.
- assertThat(pagingSources).hasSize(2)
- assertThat(pagingSources[0].onInvalidatedCallbacks).isEmpty()
- assertThat(pagingSources[1].onInvalidatedCallbacks).hasSize(1)
+ assertThat(pagingSources[0].invalidateCallbackCount).isEqualTo(0)
+ assertThat(pagingSources[1].invalidateCallbackCount).isEqualTo(1)
// Trigger new generation, should unregister from older PagingSource.
pageFetcher.refresh()
advanceUntilIdle()
- assertThat(pagingSources[1].onInvalidatedCallbacks).isEmpty()
- assertThat(pagingSources[2].onInvalidatedCallbacks).hasSize(1)
+ assertThat(pagingSources).hasSize(3)
+ assertThat(pagingSources[1].invalidateCallbackCount).isEqualTo(0)
+ assertThat(pagingSources[2].invalidateCallbackCount).isEqualTo(1)
state.job.cancel()
}
@@ -534,7 +690,59 @@
pagingSource!!.invalidate()
advanceUntilIdle()
- assertEquals(1, invalidatesFromAdapter)
+ // InvalidatedCallbacks added after a PagingSource is already invalid should be
+ // immediately triggered, so both listeners we add should be triggered.
+ assertEquals(2, invalidatesFromAdapter)
+ job.cancel()
+ }
+
+ @Test
+ fun pagingSourceInvalidBeforeCallbackAddedCancelsInitialLoad() = testScope.runBlockingTest {
+ val pagingSources = mutableListOf<PagingSource<Int, Int>>()
+ val loadedPages = mutableListOf<Page<Int, Int>>()
+
+ var i = 0
+ val pager = Pager(PagingConfig(10)) {
+ object : PagingSource<Int, Int>() {
+ override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Int> {
+ // Suspend and await advanceUntilIdle() before allowing load to complete.
+ delay(1000)
+ return Page.empty<Int, Int>().also {
+ loadedPages.add(it)
+ }
+ }
+
+ override fun getRefreshKey(state: PagingState<Int, Int>) = null
+ }.also {
+ pagingSources.add(it)
+
+ if (i++ == 0) {
+ it.invalidate()
+ }
+ }
+ }
+
+ @OptIn(ExperimentalStdlibApi::class)
+ val job = launch {
+ pager.flow.collectLatest { pagingData ->
+ TestPagingDataDiffer<Int>(testScope.coroutineContext[CoroutineDispatcher]!!)
+ .collectFrom(pagingData)
+ }
+ }
+
+ // First PagingSource starts immediately invalid and creates a new PagingSource, but does
+ // not finish initial page load.
+ assertThat(pagingSources).hasSize(2)
+ assertThat(pagingSources[0].invalid).isTrue()
+ assertThat(loadedPages).hasSize(0)
+
+ advanceUntilIdle()
+
+ // After draining tasks, we should immediately get a second generation which triggers
+ // page load, skipping the initial load from first generation due to cancellation.
+ assertThat(pagingSources[1].invalid).isFalse()
+ assertThat(loadedPages).hasSize(1)
+
job.cancel()
}
@@ -594,7 +802,7 @@
// Verify remote refresh is called with PagingState from first generation.
val pagingState = PagingState(
pages = listOf(
- PagingSource.LoadResult.Page(
+ Page(
data = listOf(50, 51, 52),
prevKey = 49,
nextKey = 53,
@@ -708,7 +916,7 @@
// Verify remote refresh is called with PagingState from first generation.
val pagingState = PagingState(
pages = listOf(
- PagingSource.LoadResult.Page(
+ Page(
data = listOf(50, 51, 52),
prevKey = 49,
nextKey = 53,
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..4277314 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
@@ -19,6 +19,7 @@
import androidx.paging.PagingSource.LoadParams
import androidx.paging.PagingSource.LoadResult
import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED
+import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
@@ -58,12 +59,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 +201,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,15 +224,43 @@
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)
}
}
+ @Test
+ fun registerInvalidatedCallback_triggersImmediatelyIfAlreadyInvalid() {
+ val pagingSource = TestPagingSource()
+ var invalidateCalls = 0
+
+ pagingSource.invalidate()
+ pagingSource.registerInvalidatedCallback { invalidateCalls++ }
+ assertThat(invalidateCalls).isEqualTo(1)
+ }
+
+ @Test
+ fun registerInvalidatedCallback_avoidsRetriggeringWhenCalledRecursively() {
+ val pagingSource = TestPagingSource()
+ var invalidateCalls = 0
+
+ pagingSource.registerInvalidatedCallback {
+ pagingSource.registerInvalidatedCallback { invalidateCalls++ }
+ pagingSource.invalidate()
+ pagingSource.registerInvalidatedCallback { invalidateCalls++ }
+ invalidateCalls++
+ }
+ pagingSource.invalidate()
+ assertThat(invalidateCalls).isEqualTo(3)
+ }
+
data class Key(val name: String, val id: Int)
data class Item(
@@ -256,6 +291,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 +311,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 +332,9 @@
if (error) {
error = false
throw EXCEPTION
+ } else if (invalidLoadResult) {
+ invalidLoadResult = false
+ return LoadResult.Invalid()
}
val start = findFirstIndexAfter(params.key!!)
@@ -304,6 +347,9 @@
if (error) {
error = false
throw EXCEPTION
+ } else if (invalidLoadResult) {
+ invalidLoadResult = false
+ return LoadResult.Invalid()
}
val firstIndexBefore = findFirstIndexBefore(params.key!!)
@@ -327,6 +373,10 @@
fun enqueueError() {
error = true
}
+
+ fun invalidateLoad() {
+ invalidLoadResult = true
+ }
}
class CustomException : Exception()
diff --git a/paging/common/src/test/kotlin/androidx/paging/PositionalDataSourceTest.kt b/paging/common/src/test/kotlin/androidx/paging/PositionalDataSourceTest.kt
index 97f3f4f..55cfb8f 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PositionalDataSourceTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PositionalDataSourceTest.kt
@@ -441,11 +441,11 @@
val datasource = ListDataSource(listOf(0, 1, 2))
val noopCallback = { }
datasource.addInvalidatedCallback(noopCallback)
- assert(datasource.onInvalidatedCallbacks.size == 1)
+ assert(datasource.invalidateCallbackCount == 1)
datasource.removeInvalidatedCallback { }
- assert(datasource.onInvalidatedCallbacks.size == 1)
+ assert(datasource.invalidateCallbackCount == 1)
datasource.removeInvalidatedCallback(noopCallback)
- assert(datasource.onInvalidatedCallbacks.size == 0)
+ assert(datasource.invalidateCallbackCount == 0)
}
@OptIn(ExperimentalCoroutinesApi::class)
diff --git a/paging/integration-tests/testapp/src/androidTest/kotlin/androidx/paging/integration/testapp/v3/OnPagesUpdatedTest.kt b/paging/integration-tests/testapp/src/androidTest/kotlin/androidx/paging/integration/testapp/v3/OnPagesUpdatedTest.kt
index 88cf1d6..a615eb5 100644
--- a/paging/integration-tests/testapp/src/androidTest/kotlin/androidx/paging/integration/testapp/v3/OnPagesUpdatedTest.kt
+++ b/paging/integration-tests/testapp/src/androidTest/kotlin/androidx/paging/integration/testapp/v3/OnPagesUpdatedTest.kt
@@ -22,18 +22,24 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.async
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import kotlin.coroutines.EmptyCoroutineContext
import kotlin.test.assertTrue
@LargeTest
@@ -74,13 +80,11 @@
}
// Trigger page update.
- scenario.onActivity { adapter.refresh() }
- adapter.awaitRefreshIdle()
+ adapter.refreshAndAwaitIdle()
onPagesUpdatedEventsCh.receiveWithTimeoutMillis(10_000)
// Trigger page update while still processing previous one, this should get buffered.
- scenario.onActivity { adapter.refresh() }
- adapter.awaitRefreshIdle()
+ adapter.refreshAndAwaitIdle()
// Ensure we are still waiting for processNextPageUpdateCh to emit to continue.
assertTrue { onPagesUpdatedEventsCh.isEmpty }
@@ -91,8 +95,7 @@
// Trigger a bunch of updates without unblocking page update collector.
repeat(66) {
- scenario.onActivity { adapter.refresh() }
- adapter.awaitRefreshIdle()
+ adapter.refreshAndAwaitIdle()
}
// Fully unblock collector.
@@ -119,7 +122,41 @@
withTimeout(timeoutMillis) { receive() }
}
- private suspend fun V3Adapter.awaitRefreshIdle() {
- loadStateFlow.first { it.source.refresh !is LoadState.Loading }
+ @OptIn(ExperimentalCoroutinesApi::class)
+ private suspend fun V3Adapter.refreshAndAwaitIdle() {
+ val scenario = scenarioRule.scenario
+ val loadStateCollectorStarted = CompletableDeferred<Unit>()
+ val result = CoroutineScope(EmptyCoroutineContext).async {
+ loadStateFlow
+ .onStart { loadStateCollectorStarted.complete(Unit) }
+ .scan(RefreshState.INITIAL) { acc, next ->
+ when (acc) {
+ RefreshState.INITIAL -> {
+ if (next.source.refresh is LoadState.Loading) {
+ RefreshState.LOADING
+ } else {
+ RefreshState.INITIAL
+ }
+ }
+ RefreshState.LOADING -> {
+ if (next.source.refresh !is LoadState.Loading) {
+ RefreshState.DONE
+ } else {
+ RefreshState.LOADING
+ }
+ }
+ else -> acc
+ }
+ }
+ .first { it == RefreshState.DONE }
+ }
+
+ loadStateCollectorStarted.await()
+ scenario.onActivity { refresh() }
+ result.await()
+ }
+
+ private enum class RefreshState {
+ INITIAL, LOADING, DONE
}
}
\ No newline at end of file
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/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListBuilderTest.kt b/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListBuilderTest.kt
index 5caf5f0..4a5dfdf 100644
--- a/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListBuilderTest.kt
+++ b/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListBuilderTest.kt
@@ -24,6 +24,7 @@
import androidx.paging.LoadState.Error
import androidx.paging.LoadState.Loading
import androidx.paging.LoadState.NotLoading
+import androidx.paging.LoadType.APPEND
import androidx.paging.LoadType.REFRESH
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
@@ -94,7 +95,10 @@
throwable = EXCEPTION
}
- private inner class MockPagingSource : PagingSource<Int, String>() {
+ inner class MockPagingSource : PagingSource<Int, String>() {
+
+ var invalidInitialLoadResult = false
+
override suspend fun load(params: LoadParams<Int>) = when (params) {
is LoadParams.Refresh -> loadInitial(params)
else -> loadRange()
@@ -103,6 +107,10 @@
override fun getRefreshKey(state: PagingState<Int, String>): Int? = null
private fun loadInitial(params: LoadParams<Int>): LoadResult<Int, String> {
+ if (invalidInitialLoadResult) {
+ invalidInitialLoadResult = false
+ return LoadResult.Invalid()
+ }
if (params is LoadParams.Refresh) {
assertEquals(6, params.loadSize)
} else {
@@ -275,6 +283,153 @@
}
@Test
+ fun initialPagedList_invalidInitialResult() {
+ val factory = MockPagingSourceFactory()
+ val pagingSources = mutableListOf<MockPagingSourceFactory.MockPagingSource>()
+ val pagedListHolder = mutableListOf<PagedList<String>>()
+
+ val livePagedList = LivePagedListBuilder(
+ {
+ factory.create().also { pagingSource ->
+ pagingSource as MockPagingSourceFactory.MockPagingSource
+ if (pagingSources.size == 0) {
+ pagingSource.invalidInitialLoadResult = true
+ }
+ pagingSources.add(pagingSource)
+ }
+ },
+ pageSize = 2
+ )
+ .setFetchExecutor(backgroundExecutor)
+ .build()
+
+ val loadStates = mutableListOf<LoadStateEvent>()
+
+ val loadStateChangedCallback = { type: LoadType, state: LoadState ->
+ if (type == REFRESH) {
+ loadStates.add(LoadStateEvent(type, state))
+ }
+ }
+
+ livePagedList.observe(lifecycleOwner) { newList ->
+ newList.addWeakLoadStateListener(loadStateChangedCallback)
+ pagedListHolder.add(newList)
+ }
+
+ // the initial empty paged list
+ val initPagedList = pagedListHolder[0]
+ assertNotNull(initPagedList)
+ assertThat(initPagedList).isInstanceOf(InitialPagedList::class.java)
+
+ drain()
+
+ // the first pagingSource returns LoadResult.Invalid. This should invalidate the first
+ // pagingSource and generate a second source
+ assertThat(pagingSources.size).isEqualTo(2)
+ assertTrue(pagingSources[0].invalid)
+ // The second source should have the successful initial load required to create a
+ // ContiguousPagedList
+ assertThat(pagedListHolder.size).isEqualTo(2)
+ assertThat(pagedListHolder[1]).isInstanceOf(ContiguousPagedList::class.java)
+
+ assertThat(loadStates).containsExactly(
+ LoadStateEvent(
+ REFRESH,
+ NotLoading(endOfPaginationReached = false)
+ ),
+ LoadStateEvent(REFRESH, Loading),
+ // when LoadResult.Invalid is returned, REFRESH is reset back to NotLoading
+ LoadStateEvent(
+ REFRESH,
+ NotLoading(endOfPaginationReached = false)
+ ),
+ LoadStateEvent(REFRESH, Loading),
+ LoadStateEvent(
+ REFRESH,
+ NotLoading(endOfPaginationReached = false)
+ )
+ )
+ }
+
+ @Test
+ fun loadAround_invalidResult() {
+ val pagingSources = mutableListOf<TestPagingSource>()
+ val pagedLists = mutableListOf<PagedList<Int>>()
+ val factory = { TestPagingSource(loadDelay = 0).also { pagingSources.add(it) } }
+
+ val livePagedList = LivePagedListBuilder(
+ factory,
+ config = PagedList.Config.Builder()
+ .setPageSize(2)
+ .setInitialLoadSizeHint(6)
+ .setEnablePlaceholders(false)
+ .build()
+ )
+ .setFetchExecutor(backgroundExecutor)
+ .build()
+
+ val loadStates = mutableListOf<LoadStateEvent>()
+
+ val loadStateChangedCallback = { type: LoadType, state: LoadState ->
+ if (type == APPEND) {
+ loadStates.add(LoadStateEvent(type, state))
+ }
+ }
+
+ livePagedList.observeForever { newList ->
+ newList.addWeakLoadStateListener(loadStateChangedCallback)
+ pagedLists.add(newList)
+ }
+
+ drain()
+
+ // pagedLists[0] is the empty InitialPagedList, we don't care about it here
+ val pagedList1 = pagedLists[1]
+ // initial load 6 items
+ assertEquals(listOf(0, 1, 2, 3, 4, 5), pagedList1)
+ // append 2 items after initial load
+ pagedList1.loadAround(5)
+
+ drain()
+ // should load 6 + 2 = 8 items
+ assertEquals(listOf(0, 1, 2, 3, 4, 5, 6, 7), pagedList1)
+ // append 2 more items but this time, return LoadResult.Invalid
+ pagedList1.loadAround(7)
+ val source = pagedLists[1].pagingSource as TestPagingSource
+ source.nextLoadResult = PagingSource.LoadResult.Invalid()
+
+ drain()
+
+ // nothing more should be loaded from invalid paged list
+ assertEquals(listOf(0, 1, 2, 3, 4, 5, 6, 7), pagedList1)
+ // a new pagedList should be generated with a refresh load starting from index 7
+ assertEquals(listOf(7, 8, 9, 10, 11, 12), pagedLists[2])
+
+ assertThat(pagingSources.size).isEqualTo(2)
+ assertThat(pagedLists.size).isEqualTo(3)
+ assertThat(loadStates).containsExactly(
+ LoadStateEvent(
+ APPEND,
+ NotLoading(endOfPaginationReached = false)
+ ), // first empty paged list
+ LoadStateEvent(
+ APPEND,
+ NotLoading(endOfPaginationReached = false)
+ ), // second paged list
+ LoadStateEvent(APPEND, Loading), // second paged list append
+ LoadStateEvent(
+ APPEND,
+ NotLoading(endOfPaginationReached = false)
+ ), // append success
+ LoadStateEvent(APPEND, Loading), // second paged list append again but fails
+ LoadStateEvent(
+ APPEND,
+ NotLoading(endOfPaginationReached = false)
+ ) // third paged list
+ )
+ }
+
+ @Test
fun legacyPagingSourcePageSize() {
val dataSources = mutableListOf<DataSource<Int, Int>>()
val pagedLists = mutableListOf<PagedList<Int>>()
diff --git a/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListTest.kt b/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListTest.kt
index c3138ca..bee7b13 100644
--- a/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListTest.kt
+++ b/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListTest.kt
@@ -26,10 +26,14 @@
import androidx.test.filters.SmallTest
import androidx.testutils.TestDispatcher
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.test.TestCoroutineScope
+import kotlinx.coroutines.test.runBlockingTest
import org.junit.Assert.assertNotNull
import org.junit.Rule
import org.junit.Test
@@ -40,11 +44,41 @@
@RunWith(AndroidJUnit4::class)
@SmallTest
@Suppress("DEPRECATION")
+@OptIn(ExperimentalCoroutinesApi::class)
class LivePagedListTest {
@JvmField
@Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
+ private val testScope = TestCoroutineScope()
+
+ @OptIn(DelicateCoroutinesApi::class)
+ @Test
+ fun invalidPagingSourceOnInitialLoadTriggersInvalidation() {
+ var pagingSourcesCreated = 0
+ val pagingSourceFactory = {
+ when (pagingSourcesCreated++) {
+ 0 -> TestPagingSource().apply {
+ invalidate()
+ }
+ else -> TestPagingSource()
+ }
+ }
+
+ val livePagedList = LivePagedList(
+ coroutineScope = GlobalScope,
+ initialKey = null,
+ config = PagedList.Config.Builder().setPageSize(10).build(),
+ boundaryCallback = null,
+ pagingSourceFactory = pagingSourceFactory,
+ notifyDispatcher = ArchTaskExecutor.getMainThreadExecutor().asCoroutineDispatcher(),
+ fetchDispatcher = ArchTaskExecutor.getIOThreadExecutor().asCoroutineDispatcher(),
+ )
+
+ livePagedList.observeForever { }
+ assertThat(pagingSourcesCreated).isEqualTo(2)
+ }
+
@OptIn(DelicateCoroutinesApi::class)
@Test
fun instantiatesPagingSourceOnFetchDispatcher() {
@@ -167,6 +201,47 @@
}
}
+ @OptIn(ExperimentalStdlibApi::class)
+ @Test
+ fun initialLoad_loadResultInvalid() = testScope.runBlockingTest {
+ val dispatcher = coroutineContext[CoroutineDispatcher.Key]!!
+ val pagingSources = mutableListOf<TestPagingSource>()
+ val factory = {
+ TestPagingSource().also {
+ if (pagingSources.size == 0) it.nextLoadResult = PagingSource.LoadResult.Invalid()
+ pagingSources.add(it)
+ }
+ }
+ val config = PagedList.Config.Builder()
+ .setEnablePlaceholders(false)
+ .setPageSize(3)
+ .build()
+
+ val livePagedList = LivePagedList(
+ coroutineScope = testScope,
+ initialKey = null,
+ config = config,
+ boundaryCallback = null,
+ pagingSourceFactory = factory,
+ notifyDispatcher = dispatcher,
+ fetchDispatcher = dispatcher,
+ )
+
+ val pagedLists = mutableListOf<PagedList<Int>>()
+ livePagedList.observeForever {
+ pagedLists.add(it)
+ }
+
+ advanceUntilIdle()
+
+ assertThat(pagedLists.size).isEqualTo(2)
+ assertThat(pagingSources.size).isEqualTo(2)
+ assertThat(pagedLists.size).isEqualTo(2)
+ assertThat(pagedLists[1]).containsExactly(
+ 0, 1, 2, 3, 4, 5, 6, 7, 8
+ )
+ }
+
companion object {
@Suppress("DEPRECATION")
private val dataSource = object : PositionalDataSource<String>() {
diff --git a/paging/runtime/src/main/java/androidx/paging/LivePagedList.kt b/paging/runtime/src/main/java/androidx/paging/LivePagedList.kt
index ab754be..ea2f8d1f 100644
--- a/paging/runtime/src/main/java/androidx/paging/LivePagedList.kt
+++ b/paging/runtime/src/main/java/androidx/paging/LivePagedList.kt
@@ -88,6 +88,13 @@
val params = config.toRefreshLoadParams(lastKey)
when (val initialResult = pagingSource.load(params)) {
+ is PagingSource.LoadResult.Invalid -> {
+ currentData.setInitialLoadState(
+ REFRESH,
+ LoadState.NotLoading(false)
+ )
+ pagingSource.invalidate()
+ }
is PagingSource.LoadResult.Error -> {
currentData.setInitialLoadState(
REFRESH,
diff --git a/paging/rxjava2/build.gradle b/paging/rxjava2/build.gradle
index d7a7de6..459bc18 100644
--- a/paging/rxjava2/build.gradle
+++ b/paging/rxjava2/build.gradle
@@ -33,9 +33,11 @@
testImplementation(project(":internal-testutils-common"))
testImplementation(project(":internal-testutils-paging"))
+ testImplementation(project(":internal-testutils-ktx"))
testImplementation(libs.junit)
testImplementation(libs.kotlinTest)
testImplementation(libs.kotlinCoroutinesTest)
+ testImplementation(libs.truth)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
diff --git a/paging/rxjava2/src/main/java/androidx/paging/RxPagedListBuilder.kt b/paging/rxjava2/src/main/java/androidx/paging/RxPagedListBuilder.kt
index ee513d8..69cc178 100644
--- a/paging/rxjava2/src/main/java/androidx/paging/RxPagedListBuilder.kt
+++ b/paging/rxjava2/src/main/java/androidx/paging/RxPagedListBuilder.kt
@@ -404,6 +404,13 @@
val lastKey = currentData.lastKey as Key?
val params = config.toRefreshLoadParams(lastKey)
when (val initialResult = pagingSource.load(params)) {
+ is PagingSource.LoadResult.Invalid -> {
+ currentData.setInitialLoadState(
+ LoadType.REFRESH,
+ LoadState.NotLoading(endOfPaginationReached = false)
+ )
+ pagingSource.invalidate()
+ }
is PagingSource.LoadResult.Error -> {
currentData.setInitialLoadState(
LoadType.REFRESH,
diff --git a/paging/rxjava2/src/test/java/androidx/paging/RxPagedListBuilderTest.kt b/paging/rxjava2/src/test/java/androidx/paging/RxPagedListBuilderTest.kt
index 07bfe47..3d3c5c2 100644
--- a/paging/rxjava2/src/test/java/androidx/paging/RxPagedListBuilderTest.kt
+++ b/paging/rxjava2/src/test/java/androidx/paging/RxPagedListBuilderTest.kt
@@ -20,15 +20,22 @@
import androidx.paging.LoadState.Loading
import androidx.paging.LoadState.NotLoading
import androidx.paging.LoadType.REFRESH
+import androidx.testutils.DirectDispatcher
+import androidx.testutils.TestDispatcher
import io.reactivex.Observable
import io.reactivex.observers.TestObserver
import io.reactivex.schedulers.Schedulers
import io.reactivex.schedulers.TestScheduler
+import kotlinx.coroutines.DelicateCoroutinesApi
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import kotlin.test.assertTrue
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.withContext
@Suppress("DEPRECATION")
@RunWith(JUnit4::class)
@@ -53,8 +60,10 @@
}
class MockDataSourceFactory {
- fun create(): PagingSource<Int, String> {
- return MockPagingSource()
+ fun create(
+ loadDispatcher: CoroutineDispatcher = DirectDispatcher
+ ): PagingSource<Int, String> {
+ return MockPagingSource(loadDispatcher)
}
var throwable: Throwable? = null
@@ -63,10 +72,25 @@
throwable = EXCEPTION
}
- private inner class MockPagingSource : PagingSource<Int, String>() {
- override suspend fun load(params: LoadParams<Int>) = when (params) {
- is LoadParams.Refresh -> loadInitial(params)
- else -> loadRange()
+ inner class MockPagingSource(
+ // Allow explicit control of load calls outside of fetch / notify. Note: This is
+ // different from simply setting fetchDispatcher because PagingObservableOnSubscribe
+ // init happens on fetchDispatcher which makes it difficult to differentiate
+ // InitialPagedList.
+ val loadDispatcher: CoroutineDispatcher
+ ) : PagingSource<Int, String>() {
+ var invalidInitialLoad = false
+
+ override suspend fun load(params: LoadParams<Int>): LoadResult<Int, String> {
+ return withContext(loadDispatcher) {
+ if (invalidInitialLoad) {
+ invalidInitialLoad = false
+ LoadResult.Invalid()
+ } else when (params) {
+ is LoadParams.Refresh -> loadInitial(params)
+ else -> loadRange()
+ }
+ }
}
override fun getRefreshKey(state: PagingState<Int, String>): Int? = null
@@ -256,6 +280,105 @@
}
@Test
+ fun observablePagedList_invalidInitialResult() {
+ // this TestDispatcher is used to queue up pagingSource.load(). This allows us to control
+ // and assert against each load() attempt outside of fetch/notify dispatcher
+ val loadDispatcher = TestDispatcher()
+ val pagingSources = mutableListOf<MockDataSourceFactory.MockPagingSource>()
+ val factory = {
+ MockDataSourceFactory().create(loadDispatcher).also {
+ val source = it as MockDataSourceFactory.MockPagingSource
+ if (pagingSources.size == 0) source.invalidInitialLoad = true
+ pagingSources.add(source)
+ }
+ }
+ // this is essentially a direct scheduler so jobs are run immediately
+ val scheduler = Schedulers.from(DirectDispatcher.asExecutor())
+ val observable = RxPagedListBuilder(factory, 2)
+ .setFetchScheduler(scheduler)
+ .setNotifyScheduler(scheduler)
+ .buildObservable()
+
+ val observer = TestObserver<PagedList<String>>()
+ // subscribe triggers the PagingObservableOnSubscribe's invalidate() to create first
+ // pagingSource
+ observable.subscribe(observer)
+
+ // ensure the InitialPagedList with empty data is observed
+ observer.assertValueCount(1)
+ val initPagedList = observer.values()[0]!!
+ assertThat(initPagedList).isInstanceOf(InitialPagedList::class.java)
+ assertThat(initPagedList).isEmpty()
+ // ensure first pagingSource is also created at this point
+ assertThat(pagingSources.size).isEqualTo(1)
+
+ val loadStates = mutableListOf<LoadStateEvent>()
+ val loadStateChangedCallback = { type: LoadType, state: LoadState ->
+ if (type == REFRESH) {
+ loadStates.add(LoadStateEvent(type, state))
+ }
+ }
+
+ initPagedList.addWeakLoadStateListener(loadStateChangedCallback)
+
+ assertThat(loadStates).containsExactly(
+ // before first load() is called, REFRESH is set to loading, represents load
+ // attempt on first pagingSource
+ LoadStateEvent(REFRESH, Loading)
+ )
+
+ // execute first load, represents load attempt on first paging source
+ //
+ // using poll().run() instead of executeAll(), because executeAll() + immediate schedulers
+ // result in first load + subsequent loads executing immediately and we won't be able to
+ // assert the pagedLists/loads incrementally
+ loadDispatcher.queue.poll()?.run()
+
+ // the load failed so there should still be only one PagedList, but the first
+ // pagingSource should invalidated, and the second pagingSource is created
+ observer.assertValueCount(1)
+ assertTrue(pagingSources[0].invalid)
+ assertThat(pagingSources.size).isEqualTo(2)
+
+ assertThat(loadStates).containsExactly(
+ // the first load attempt
+ LoadStateEvent(REFRESH, Loading),
+ // LoadResult.Invalid resets RERFRESH state
+ LoadStateEvent(
+ REFRESH,
+ NotLoading(endOfPaginationReached = false)
+ ),
+ // before second load() is called, REFRESH is set to loading, represents load
+ // attempt on second pagingSource
+ LoadStateEvent(REFRESH, Loading),
+ )
+
+ // execute the load attempt on second pagingSource which succeeds
+ loadDispatcher.queue.poll()?.run()
+
+ // ensure second pagedList created with the correct data loaded
+ observer.assertValueCount(2)
+ val secondPagedList = observer.values()[1]
+ assertThat(secondPagedList).containsExactly("a", "b", null, null)
+ assertThat(secondPagedList).isNotInstanceOf(InitialPagedList::class.java)
+ assertThat(secondPagedList).isInstanceOf(ContiguousPagedList::class.java)
+
+ secondPagedList.addWeakLoadStateListener(loadStateChangedCallback)
+ assertThat(loadStates).containsExactly(
+ LoadStateEvent(REFRESH, Loading), // first load
+ LoadStateEvent(
+ REFRESH,
+ NotLoading(endOfPaginationReached = false)
+ ), // first load reset
+ LoadStateEvent(REFRESH, Loading), // second load
+ LoadStateEvent(
+ REFRESH,
+ NotLoading(endOfPaginationReached = false)
+ ), // second load succeeds
+ )
+ }
+
+ @Test
fun instantiatesPagingSourceOnFetchDispatcher() {
var pagingSourcesCreated = 0
val pagingSourceFactory = {
@@ -297,6 +420,33 @@
rxPagedList.firstOrError().blockingGet().dataSource
}
+ @OptIn(DelicateCoroutinesApi::class)
+ @Test
+ fun invalidPagingSourceOnInitialLoadTriggersInvalidation() {
+ var pagingSourcesCreated = 0
+ val pagingSourceFactory = {
+ when (pagingSourcesCreated++) {
+ 0 -> TestPagingSource().apply {
+ invalidate()
+ }
+ else -> TestPagingSource()
+ }
+ }
+
+ val testScheduler = TestScheduler()
+ val rxPagedList = RxPagedListBuilder(
+ pageSize = 10,
+ pagingSourceFactory = pagingSourceFactory,
+ ).apply {
+ setNotifyScheduler(testScheduler)
+ setFetchScheduler(testScheduler)
+ }.buildObservable()
+
+ rxPagedList.subscribe()
+ testScheduler.triggerActions()
+ assertEquals(2, pagingSourcesCreated)
+ }
+
companion object {
val EXCEPTION = Exception("")
}
diff --git a/paging/rxjava3/build.gradle b/paging/rxjava3/build.gradle
index 5b5bf91..5755ccd 100644
--- a/paging/rxjava3/build.gradle
+++ b/paging/rxjava3/build.gradle
@@ -33,9 +33,11 @@
testImplementation(project(":internal-testutils-common"))
testImplementation(project(":internal-testutils-paging"))
+ testImplementation(project(":internal-testutils-ktx"))
testImplementation(libs.junit)
testImplementation(libs.kotlinTest)
testImplementation(libs.kotlinCoroutinesTest)
+ testImplementation(libs.truth)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
diff --git a/paging/rxjava3/src/main/java/androidx/paging/rxjava3/RxPagedListBuilder.kt b/paging/rxjava3/src/main/java/androidx/paging/rxjava3/RxPagedListBuilder.kt
index 289b9b4a..d720c98 100644
--- a/paging/rxjava3/src/main/java/androidx/paging/rxjava3/RxPagedListBuilder.kt
+++ b/paging/rxjava3/src/main/java/androidx/paging/rxjava3/RxPagedListBuilder.kt
@@ -412,6 +412,13 @@
val lastKey = currentData.lastKey as Key?
val params = config.toRefreshLoadParams(lastKey)
when (val initialResult = pagingSource.load(params)) {
+ is PagingSource.LoadResult.Invalid -> {
+ currentData.setInitialLoadState(
+ LoadType.REFRESH,
+ LoadState.NotLoading(endOfPaginationReached = false)
+ )
+ pagingSource.invalidate()
+ }
is PagingSource.LoadResult.Error -> {
currentData.setInitialLoadState(
LoadType.REFRESH,
diff --git a/paging/rxjava3/src/test/java/androidx/paging/RxPagedListBuilderTest.kt b/paging/rxjava3/src/test/java/androidx/paging/RxPagedListBuilderTest.kt
index 091672a..0a3317b 100644
--- a/paging/rxjava3/src/test/java/androidx/paging/RxPagedListBuilderTest.kt
+++ b/paging/rxjava3/src/test/java/androidx/paging/RxPagedListBuilderTest.kt
@@ -23,15 +23,22 @@
import androidx.paging.LoadState.NotLoading
import androidx.paging.LoadType.REFRESH
import androidx.paging.rxjava3.RxPagedListBuilder
+import androidx.testutils.DirectDispatcher
+import androidx.testutils.TestDispatcher
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.observers.TestObserver
import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.schedulers.TestScheduler
+import kotlinx.coroutines.DelicateCoroutinesApi
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import kotlin.test.assertTrue
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.withContext
@RunWith(JUnit4::class)
class RxPagedListBuilderTest {
@@ -55,8 +62,10 @@
}
class MockDataSourceFactory {
- fun create(): PagingSource<Int, String> {
- return MockPagingSource()
+ fun create(
+ loadDispatcher: CoroutineDispatcher = DirectDispatcher
+ ): PagingSource<Int, String> {
+ return MockPagingSource(loadDispatcher)
}
var throwable: Throwable? = null
@@ -65,10 +74,25 @@
throwable = EXCEPTION
}
- private inner class MockPagingSource : PagingSource<Int, String>() {
- override suspend fun load(params: LoadParams<Int>) = when (params) {
- is LoadParams.Refresh -> loadInitial(params)
- else -> loadRange()
+ inner class MockPagingSource(
+ // Allow explicit control of load calls outside of fetch / notify. Note: This is
+ // different from simply setting fetchDispatcher because PagingObservableOnSubscribe
+ // init happens on fetchDispatcher which makes it difficult to differentiate
+ // InitialPagedList.
+ val loadDispatcher: CoroutineDispatcher
+ ) : PagingSource<Int, String>() {
+ var invalidInitialLoad = false
+
+ override suspend fun load(params: LoadParams<Int>): LoadResult<Int, String> {
+ return withContext(loadDispatcher) {
+ if (invalidInitialLoad) {
+ invalidInitialLoad = false
+ LoadResult.Invalid()
+ } else when (params) {
+ is LoadParams.Refresh -> loadInitial(params)
+ else -> loadRange()
+ }
+ }
}
override fun getRefreshKey(state: PagingState<Int, String>): Int? = null
@@ -258,6 +282,105 @@
}
@Test
+ fun observablePagedList_invalidInitialResult() {
+ // this TestDispatcher is used to queue up pagingSource.load(). This allows us to control
+ // and assert against each load() attempt outside of fetch/notify dispatcher
+ val loadDispatcher = TestDispatcher()
+ val pagingSources = mutableListOf<MockDataSourceFactory.MockPagingSource>()
+ val factory = {
+ MockDataSourceFactory().create(loadDispatcher).also {
+ val source = it as MockDataSourceFactory.MockPagingSource
+ if (pagingSources.size == 0) source.invalidInitialLoad = true
+ pagingSources.add(source)
+ }
+ }
+ // this is essentially a direct scheduler so jobs are run immediately
+ val scheduler = Schedulers.from(DirectDispatcher.asExecutor())
+ val observable = RxPagedListBuilder(factory, 2)
+ .setFetchScheduler(scheduler)
+ .setNotifyScheduler(scheduler)
+ .buildObservable()
+
+ val observer = TestObserver<PagedList<String>>()
+ // subscribe triggers the PagingObservableOnSubscribe's invalidate() to create first
+ // pagingSource
+ observable.subscribe(observer)
+
+ // ensure the InitialPagedList with empty data is observed
+ observer.assertValueCount(1)
+ val initPagedList = observer.values()[0]!!
+ assertThat(initPagedList).isInstanceOf(InitialPagedList::class.java)
+ assertThat(initPagedList).isEmpty()
+ // ensure first pagingSource is also created at this point
+ assertThat(pagingSources.size).isEqualTo(1)
+
+ val loadStates = mutableListOf<LoadStateEvent>()
+ val loadStateChangedCallback = { type: LoadType, state: LoadState ->
+ if (type == REFRESH) {
+ loadStates.add(LoadStateEvent(type, state))
+ }
+ }
+
+ initPagedList.addWeakLoadStateListener(loadStateChangedCallback)
+
+ assertThat(loadStates).containsExactly(
+ // before first load() is called, REFRESH is set to loading, represents load
+ // attempt on first pagingSource
+ LoadStateEvent(REFRESH, Loading)
+ )
+
+ // execute first load, represents load attempt on first paging source
+ //
+ // using poll().run() instead of executeAll(), because executeAll() + immediate schedulers
+ // result in first load + subsequent loads executing immediately and we won't be able to
+ // assert the pagedLists/loads incrementally
+ loadDispatcher.queue.poll()?.run()
+
+ // the load failed so there should still be only one PagedList, but the first
+ // pagingSource should invalidated, and the second pagingSource is created
+ observer.assertValueCount(1)
+ assertTrue(pagingSources[0].invalid)
+ assertThat(pagingSources.size).isEqualTo(2)
+
+ assertThat(loadStates).containsExactly(
+ // the first load attempt
+ LoadStateEvent(REFRESH, Loading),
+ // LoadResult.Invalid resets RERFRESH state
+ LoadStateEvent(
+ REFRESH,
+ NotLoading(endOfPaginationReached = false)
+ ),
+ // before second load() is called, REFRESH is set to loading, represents load
+ // attempt on second pagingSource
+ LoadStateEvent(REFRESH, Loading),
+ )
+
+ // execute the load attempt on second pagingSource which succeeds
+ loadDispatcher.queue.poll()?.run()
+
+ // ensure second pagedList created with the correct data loaded
+ observer.assertValueCount(2)
+ val secondPagedList = observer.values()[1]
+ assertThat(secondPagedList).containsExactly("a", "b", null, null)
+ assertThat(secondPagedList).isNotInstanceOf(InitialPagedList::class.java)
+ assertThat(secondPagedList).isInstanceOf(ContiguousPagedList::class.java)
+
+ secondPagedList.addWeakLoadStateListener(loadStateChangedCallback)
+ assertThat(loadStates).containsExactly(
+ LoadStateEvent(REFRESH, Loading), // first load
+ LoadStateEvent(
+ REFRESH,
+ NotLoading(endOfPaginationReached = false)
+ ), // first load reset
+ LoadStateEvent(REFRESH, Loading), // second load
+ LoadStateEvent(
+ REFRESH,
+ NotLoading(endOfPaginationReached = false)
+ ), // second load succeeds
+ )
+ }
+
+ @Test
fun instantiatesPagingSourceOnFetchDispatcher() {
var pagingSourcesCreated = 0
val pagingSourceFactory = {
@@ -299,6 +422,33 @@
rxPagedList.firstOrError().blockingGet().dataSource
}
+ @OptIn(DelicateCoroutinesApi::class)
+ @Test
+ fun invalidPagingSourceOnInitialLoadTriggersInvalidation() {
+ var pagingSourcesCreated = 0
+ val pagingSourceFactory = {
+ when (pagingSourcesCreated++) {
+ 0 -> TestPagingSource().apply {
+ invalidate()
+ }
+ else -> TestPagingSource()
+ }
+ }
+
+ val testScheduler = TestScheduler()
+ val rxPagedList = RxPagedListBuilder(
+ pageSize = 10,
+ pagingSourceFactory = pagingSourceFactory,
+ ).apply {
+ setNotifyScheduler(testScheduler)
+ setFetchScheduler(testScheduler)
+ }.buildObservable()
+
+ rxPagedList.subscribe()
+ testScheduler.triggerActions()
+ assertEquals(2, pagingSourcesCreated)
+ }
+
companion object {
val EXCEPTION = Exception("")
}
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/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/PagingSourceTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/PagingSourceTest.kt
index 77a0e9b..d3f0f18 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/PagingSourceTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/PagingSourceTest.kt
@@ -69,6 +69,8 @@
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
+import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
/**
* This test intentionally uses real dispatchers to mimic the real use case.
@@ -82,6 +84,7 @@
private lateinit var itemStore: ItemStore
private val queryExecutor = FilteringExecutor()
private val mainThreadQueries = mutableListOf<Pair<String, String>>()
+ private val pagingSources = mutableListOf<PagingSource<Int, PagingEntity>>()
@Before
fun init() {
@@ -115,6 +118,7 @@
// Check no mainThread queries happened.
assertThat(mainThreadQueries).isEmpty()
coroutineScope.cancel()
+ pagingSources.clear()
}
@Test
@@ -220,25 +224,16 @@
)
// make sure invalidation requests a refresh
db.invalidationTracker.awaitPendingRefresh()
- // make sure we blocked the refresh runnable to ensure test makes sense
+ // make sure we blocked the refresh runnable until after the exception generates a
+ // new paging source
queryExecutor.awaitDeferredSizeAtLeast(1)
- /** TODO(b/191806126): This .get() call triggers a page fetch on the first generation
- * although we wrote to DB because invalidation tracker hasn't invalidate the
- * PagingSource yet. Currently we return a best-effort page, but this could
- * lead to UI inconsistencies. In this particular scenario,
- * LimitOffsetPagingSource returns item 89 but presenter doesn't update.
- */
+ // Now get more items. The pagingSource's load() will check for invalidation and then
+ // return LoadResult.Invalid, causing a second generation paging source to be generated.
itemStore.get(70)
- /**
- * Unlike old room-paging implementation through LimitOffsetDataSource, new
- * implementation relies on InvalidationTracker to invalidate paging sources, so we
- * need to execute the deferred refresh runnable to invalidate the paging source.
- */
- queryExecutor.executeAll()
-
itemStore.awaitGeneration(2)
+ assertTrue(pagingSources[0].invalid)
itemStore.awaitInitialLoad()
// it might be reloaded in any range so just make sure everything is there
assertThat(itemStore.peekItems()).hasSize(10)
@@ -255,6 +250,28 @@
items[80 + it]
)
}
+ // Runs deferred invalidationTracker.refreshRunnable. Note that the step in
+ // itemStore.get(70) includes checking the invalidation tables & resetting the tracker's
+ // pendingRefresh flag to false.
+ // Therefore, the mRefreshRunnable executed by executeAll() will not detect changes
+ // in the table anymore.
+ assertThat(db.invalidationTracker.pendingRefresh).isFalse()
+ queryExecutor.executeAll()
+
+ itemStore.awaitInitialLoad()
+
+ // make sure only two generations of paging sources have been created
+ assertTrue(!pagingSources[1].invalid)
+
+ // if a third generation is created, awaitGeneration(3) will return instead of timing
+ // out.
+ val expectError = assertFailsWith<AssertionError> {
+ itemStore.awaitGeneration(3)
+ }
+ assertThat(expectError.message).isEqualTo("didn't complete in expected time")
+
+ assertThat(itemStore.currentGenerationId).isEqualTo(2)
+ assertThat(pagingSources.size).isEqualTo(2)
}
}
@@ -283,7 +300,7 @@
pager: Pager<Int, PagingEntity> =
Pager(
config = CONFIG,
- pagingSourceFactory = db.dao::loadItems
+ pagingSourceFactory = { db.dao.loadItems().also { pagingSources.add(it) } }
),
block: suspend () -> Unit
) {
@@ -559,7 +576,7 @@
private suspend fun <T> withTestTimeout(block: suspend () -> T): T {
try {
return withTimeout(
- timeMillis = TimeUnit.SECONDS.toMillis(10)
+ timeMillis = TimeUnit.SECONDS.toMillis(3)
) {
block()
}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/MusicDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/MusicDao.java
index c075fc2..49d8e86 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/MusicDao.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/MusicDao.java
@@ -35,6 +35,10 @@
import androidx.room.integration.testapp.vo.Song;
import androidx.sqlite.db.SupportSQLiteQuery;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSetMultimap;
+
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -128,4 +132,51 @@
@Query("SELECT * FROM Artist JOIN Song ON Artist.mArtistName = Song.mArtist")
Flowable<Map<Artist, Set<Song>>> getAllArtistAndTheirSongsAsFlowableSet();
+
+ /* Guava ImmutableMap */
+ @Query("SELECT * FROM Artist JOIN Song ON Artist.mArtistName = Song.mArtist")
+ ImmutableMap<Artist, List<Song>> getAllArtistAndTheirSongsImmutableMap();
+
+ @RawQuery
+ ImmutableMap<Artist, List<Song>> getAllArtistAndTheirSongsRawQueryImmutableMap(
+ SupportSQLiteQuery query
+ );
+
+ @Query("SELECT * FROM Artist JOIN Song ON Artist.mArtistName = Song.mArtist")
+ LiveData<ImmutableMap<Artist, Set<Song>>> getAllArtistAndTheirSongsAsLiveDataImmutableMap();
+
+ /* Guava Multimap */
+ @Query("SELECT * FROM Artist JOIN Song ON Artist.mArtistName = Song.mArtist")
+ ImmutableSetMultimap<Artist, Song> getAllArtistAndTheirSongsGuavaImmutableSetMultimap();
+
+ @Query("SELECT * FROM Artist JOIN Song ON Artist.mArtistName = Song.mArtist")
+ ImmutableListMultimap<Artist, Song> getAllArtistAndTheirSongsGuavaImmutableListMultimap();
+
+ @Transaction
+ @Query("SELECT * FROM Artist JOIN Album ON Artist.mArtistName = Album.mAlbumArtist")
+ ImmutableSetMultimap<Artist, AlbumWithSongs>
+ getAllArtistAndTheirAlbumsWithSongsGuavaImmutableSetMultimap();
+
+ @Transaction
+ @Query("SELECT * FROM Artist JOIN Album ON Artist.mArtistName = Album.mAlbumArtist")
+ ImmutableListMultimap<Artist, AlbumWithSongs>
+ getAllArtistAndTheirAlbumsWithSongsGuavaImmutableListMultimap();
+
+ @RawQuery
+ ImmutableSetMultimap<Artist, Song> getAllArtistAndTheirSongsRawQueryGuavaImmutableSetMultimap(
+ SupportSQLiteQuery query
+ );
+
+ @RawQuery
+ ImmutableListMultimap<Artist, Song> getAllArtistAndTheirSongsRawQueryGuavaImmutableListMultimap(
+ SupportSQLiteQuery query
+ );
+
+ @Query("SELECT * FROM Artist JOIN Song ON Artist.mArtistName = Song.mArtist")
+ LiveData<ImmutableSetMultimap<Artist, Song>>
+ getAllArtistAndTheirSongsAsLiveDataGuavaImmutableSetMultimap();
+
+ @Query("SELECT * FROM Artist JOIN Song ON Artist.mArtistName = Song.mArtist")
+ LiveData<ImmutableListMultimap<Artist, Song>>
+ getAllArtistAndTheirSongsAsLiveDataGuavaImmutableListMultimap();
}
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MultimapQueryTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MultimapQueryTest.java
index 30e46e4..e6606a0 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MultimapQueryTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MultimapQueryTest.java
@@ -41,6 +41,13 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+
import org.hamcrest.MatcherAssert;
import org.junit.Before;
import org.junit.Rule;
@@ -472,6 +479,215 @@
}
}
+ @Test
+ public void testJoinByArtistNameGuavaImmutableListMultimap() {
+ mMusicDao.addSongs(mRhcpSong1, mRhcpSong2, mAcdcSong1, mPinkFloydSong1);
+ mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd);
+
+ ImmutableListMultimap<Artist, Song> artistToSongs =
+ mMusicDao.getAllArtistAndTheirSongsGuavaImmutableListMultimap();
+ assertContentsOfResultMultimap(artistToSongs);
+ }
+
+ @Test
+ public void testJoinByArtistNameGuavaImmutableSetMultimap() {
+ mMusicDao.addSongs(mRhcpSong1, mRhcpSong2, mAcdcSong1, mPinkFloydSong1);
+ mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd);
+
+ ImmutableSetMultimap<Artist, Song> artistToSongs =
+ mMusicDao.getAllArtistAndTheirSongsGuavaImmutableSetMultimap();
+ assertContentsOfResultMultimap(artistToSongs);
+ }
+
+ @Test
+ public void testJoinByArtistNameRawQueryGuavaImmutableListMultimap() {
+ mMusicDao.addSongs(mRhcpSong1, mRhcpSong2, mAcdcSong1, mPinkFloydSong1);
+ mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd);
+
+ ImmutableListMultimap<Artist, Song> artistToSongsMap =
+ mMusicDao.getAllArtistAndTheirSongsRawQueryGuavaImmutableListMultimap(
+ new SimpleSQLiteQuery(
+ "SELECT * FROM Artist JOIN Song ON Artist.mArtistName = Song"
+ + ".mArtist"
+ )
+ );
+ assertThat(artistToSongsMap.get(mAcDc)).containsExactly(mAcdcSong1);
+ }
+
+ @Test
+ public void testJoinByArtistNameRawQueryGuavaImmutableSetMultimap() {
+ mMusicDao.addSongs(mRhcpSong1, mRhcpSong2, mAcdcSong1, mPinkFloydSong1);
+ mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd);
+
+ ImmutableSetMultimap<Artist, Song> artistToSongsMap =
+ mMusicDao.getAllArtistAndTheirSongsRawQueryGuavaImmutableSetMultimap(
+ new SimpleSQLiteQuery(
+ "SELECT * FROM Artist JOIN Song ON Artist.mArtistName = Song"
+ + ".mArtist"
+ )
+ );
+ assertThat(artistToSongsMap.get(mAcDc)).containsExactly(mAcdcSong1);
+ }
+
+ @Test
+ public void testJoinByArtistNameLiveDataGuavaImmutableListMultimap()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ mMusicDao.addSongs(mRhcpSong1, mRhcpSong2, mAcdcSong1, mPinkFloydSong1);
+ mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd);
+
+ LiveData<ImmutableListMultimap<Artist, Song>> artistToSongsMapLiveData =
+ mMusicDao.getAllArtistAndTheirSongsAsLiveDataGuavaImmutableListMultimap();
+ final TestLifecycleOwner testOwner = new TestLifecycleOwner(Lifecycle.State.CREATED);
+ final TestObserver<ImmutableListMultimap<Artist, Song>> observer = new MyTestObserver<>();
+ TestUtil.observeOnMainThread(artistToSongsMapLiveData, testOwner, observer);
+ MatcherAssert.assertThat(observer.hasValue(), is(false));
+ observer.reset();
+ testOwner.handleLifecycleEvent(Lifecycle.Event.ON_START);
+
+ assertThat(observer.get()).isNotNull();
+ assertContentsOfResultMultimap(observer.get());
+ }
+
+ @Test
+ public void testJoinByArtistNameLiveDataGuavaImmutableSetMultimap()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ mMusicDao.addSongs(mRhcpSong1, mRhcpSong2, mAcdcSong1, mPinkFloydSong1);
+ mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd);
+
+ LiveData<ImmutableSetMultimap<Artist, Song>> artistToSongsMapLiveData =
+ mMusicDao.getAllArtistAndTheirSongsAsLiveDataGuavaImmutableSetMultimap();
+ final TestLifecycleOwner testOwner = new TestLifecycleOwner(Lifecycle.State.CREATED);
+ final TestObserver<ImmutableSetMultimap<Artist, Song>> observer = new MyTestObserver<>();
+ TestUtil.observeOnMainThread(artistToSongsMapLiveData, testOwner, observer);
+ MatcherAssert.assertThat(observer.hasValue(), is(false));
+ observer.reset();
+ testOwner.handleLifecycleEvent(Lifecycle.Event.ON_START);
+
+ assertThat(observer.get()).isNotNull();
+ assertContentsOfResultMultimap(observer.get());
+ }
+
+ @Test
+ public void testPojoWithEmbeddedAndRelationGuavaImmutableListMultimap() {
+ mMusicDao.addSongs(mRhcpSong1, mRhcpSong2, mAcdcSong1, mPinkFloydSong1);
+ mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd);
+ mMusicDao.addAlbums(
+ mStadiumArcadium,
+ mCalifornication,
+ mTheDarkSideOfTheMoon,
+ mHighwayToHell
+ );
+
+ ImmutableListMultimap<Artist, AlbumWithSongs> artistToAlbumsWithSongsMap =
+ mMusicDao.getAllArtistAndTheirAlbumsWithSongsGuavaImmutableListMultimap();
+ ImmutableList<AlbumWithSongs> rhcpList = artistToAlbumsWithSongsMap.get(mRhcp);
+
+ assertThat(artistToAlbumsWithSongsMap.keySet()).containsExactlyElementsIn(
+ Arrays.asList(mRhcp, mAcDc, mPinkFloyd));
+ assertThat(artistToAlbumsWithSongsMap.containsKey(mTheClash)).isFalse();
+ assertThat(artistToAlbumsWithSongsMap.get(
+ mPinkFloyd).get(0).getAlbum())
+ .isEqualTo(mTheDarkSideOfTheMoon);
+ assertThat(artistToAlbumsWithSongsMap.get(mAcDc).get(0).getAlbum())
+ .isEqualTo(mHighwayToHell);
+ assertThat(artistToAlbumsWithSongsMap.get(mAcDc).get(0).getSongs()
+ .get(0)).isEqualTo(mAcdcSong1);
+
+ for (AlbumWithSongs albumAndSong : rhcpList) {
+ if (albumAndSong.getAlbum().equals(mStadiumArcadium)) {
+ assertThat(albumAndSong.getSongs()).containsExactlyElementsIn(
+ Arrays.asList(mRhcpSong1, mRhcpSong2)
+ );
+ } else if (albumAndSong.getAlbum().equals(mCalifornication)) {
+ assertThat(albumAndSong.getSongs()).isEmpty();
+ } else {
+ fail();
+ }
+ }
+ }
+
+ @Test
+ public void testPojoWithEmbeddedAndRelationGuavaImmutableSetMultimap() {
+ mMusicDao.addSongs(mRhcpSong1, mRhcpSong2, mAcdcSong1, mPinkFloydSong1);
+ mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd);
+ mMusicDao.addAlbums(
+ mStadiumArcadium,
+ mCalifornication,
+ mTheDarkSideOfTheMoon,
+ mHighwayToHell
+ );
+
+ ImmutableSetMultimap<Artist, AlbumWithSongs> artistToAlbumsWithSongsMap =
+ mMusicDao.getAllArtistAndTheirAlbumsWithSongsGuavaImmutableSetMultimap();
+ ImmutableSet<AlbumWithSongs> rhcpList = artistToAlbumsWithSongsMap.get(mRhcp);
+
+ assertThat(artistToAlbumsWithSongsMap.keySet()).containsExactlyElementsIn(
+ Arrays.asList(mRhcp, mAcDc, mPinkFloyd));
+ assertThat(artistToAlbumsWithSongsMap.containsKey(mTheClash)).isFalse();
+ assertThat(artistToAlbumsWithSongsMap.get(
+ mPinkFloyd).asList().get(0).getAlbum())
+ .isEqualTo(mTheDarkSideOfTheMoon);
+ assertThat(artistToAlbumsWithSongsMap.get(mAcDc).asList().get(0).getAlbum())
+ .isEqualTo(mHighwayToHell);
+ assertThat(artistToAlbumsWithSongsMap.get(mAcDc).asList().get(0).getSongs()
+ .get(0)).isEqualTo(mAcdcSong1);
+
+ for (AlbumWithSongs albumAndSong : rhcpList) {
+ if (albumAndSong.getAlbum().equals(mStadiumArcadium)) {
+ assertThat(albumAndSong.getSongs()).containsExactlyElementsIn(
+ Arrays.asList(mRhcpSong1, mRhcpSong2)
+ );
+ } else if (albumAndSong.getAlbum().equals(mCalifornication)) {
+ assertThat(albumAndSong.getSongs()).isEmpty();
+ } else {
+ fail();
+ }
+ }
+ }
+
+ @Test
+ public void testJoinByArtistNameImmutableMap() {
+ mMusicDao.addSongs(mRhcpSong1, mRhcpSong2, mAcdcSong1, mPinkFloydSong1);
+ mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd);
+
+ ImmutableMap<Artist, List<Song>> artistToSongsMap =
+ mMusicDao.getAllArtistAndTheirSongsImmutableMap();
+ assertContentsOfResultMapWithList(artistToSongsMap);
+ }
+
+ @Test
+ public void testJoinByArtistNameRawQueryImmutableMap() {
+ mMusicDao.addSongs(mRhcpSong1, mRhcpSong2, mAcdcSong1, mPinkFloydSong1);
+ mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd);
+ ImmutableMap<Artist, List<Song>> artistToSongsMap =
+ mMusicDao.getAllArtistAndTheirSongsRawQueryImmutableMap(
+ new SimpleSQLiteQuery(
+ "SELECT * FROM Artist JOIN Song ON Artist.mArtistName = Song"
+ + ".mArtist"
+ )
+ );
+ assertContentsOfResultMapWithList(artistToSongsMap);
+ }
+
+ @Test
+ public void testJoinByArtistNameImmutableMapWithSet()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ mMusicDao.addSongs(mRhcpSong1, mRhcpSong2, mAcdcSong1, mPinkFloydSong1);
+ mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd);
+
+ LiveData<ImmutableMap<Artist, Set<Song>>> artistToSongsMapLiveData =
+ mMusicDao.getAllArtistAndTheirSongsAsLiveDataImmutableMap();
+ final TestLifecycleOwner testOwner = new TestLifecycleOwner(Lifecycle.State.CREATED);
+ final TestObserver<Map<Artist, Set<Song>>> observer = new MyTestObserver<>();
+ TestUtil.observeOnMainThread(artistToSongsMapLiveData, testOwner, observer);
+ MatcherAssert.assertThat(observer.hasValue(), is(false));
+ observer.reset();
+ testOwner.handleLifecycleEvent(Lifecycle.Event.ON_START);
+
+ assertThat(observer.get()).isNotNull();
+ assertContentsOfResultMapWithSet(observer.get());
+ }
+
/**
* Checks that the contents of the map are as expected.
*
@@ -503,4 +719,20 @@
);
assertThat(artistToSongsMap.get(mAcDc)).containsExactly(mAcdcSong1);
}
+
+ /**
+ * Checks that the contents of the map are as expected.
+ *
+ * @param artistToSongsMap Map of Artists to Collection of Songs joined by the artist name
+ */
+ private void assertContentsOfResultMultimap(ImmutableMultimap<Artist, Song> artistToSongsMap) {
+ assertThat(artistToSongsMap.keySet()).containsExactlyElementsIn(
+ Arrays.asList(mRhcp, mAcDc, mPinkFloyd));
+ assertThat(artistToSongsMap.containsKey(mTheClash)).isFalse();
+ assertThat(artistToSongsMap.get(mPinkFloyd)).containsExactly(mPinkFloydSong1);
+ assertThat(artistToSongsMap.get(mRhcp)).containsExactlyElementsIn(
+ Arrays.asList(mRhcpSong1, mRhcpSong2)
+ );
+ assertThat(artistToSongsMap.get(mAcDc)).containsExactly(mAcdcSong1);
+ }
}
\ No newline at end of file
diff --git a/room/room-common/api/current.txt b/room/room-common/api/current.txt
index a7a1d84..3ad7e2a 100644
--- a/room/room-common/api/current.txt
+++ b/room/room-common/api/current.txt
@@ -215,6 +215,7 @@
field public static final String CANNOT_CREATE_VERIFICATION_DATABASE = "ROOM_CANNOT_CREATE_VERIFICATION_DATABASE";
field public static final String CURSOR_MISMATCH = "ROOM_CURSOR_MISMATCH";
field public static final String DEFAULT_CONSTRUCTOR = "ROOM_DEFAULT_CONSTRUCTOR";
+ field public static final String DOES_NOT_IMPLEMENT_EQUALS_HASHCODE = "ROOM_TYPE_DOES_NOT_IMPLEMENT_EQUALS_HASHCODE";
field public static final String INDEX_FROM_EMBEDDED_ENTITY_IS_DROPPED = "ROOM_EMBEDDED_ENTITY_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_EMBEDDED_FIELD_IS_DROPPED = "ROOM_EMBEDDED_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_PARENT_FIELD_IS_DROPPED = "ROOM_PARENT_FIELD_INDEX_IS_DROPPED";
diff --git a/room/room-common/api/public_plus_experimental_current.txt b/room/room-common/api/public_plus_experimental_current.txt
index a7a1d84..3ad7e2a 100644
--- a/room/room-common/api/public_plus_experimental_current.txt
+++ b/room/room-common/api/public_plus_experimental_current.txt
@@ -215,6 +215,7 @@
field public static final String CANNOT_CREATE_VERIFICATION_DATABASE = "ROOM_CANNOT_CREATE_VERIFICATION_DATABASE";
field public static final String CURSOR_MISMATCH = "ROOM_CURSOR_MISMATCH";
field public static final String DEFAULT_CONSTRUCTOR = "ROOM_DEFAULT_CONSTRUCTOR";
+ field public static final String DOES_NOT_IMPLEMENT_EQUALS_HASHCODE = "ROOM_TYPE_DOES_NOT_IMPLEMENT_EQUALS_HASHCODE";
field public static final String INDEX_FROM_EMBEDDED_ENTITY_IS_DROPPED = "ROOM_EMBEDDED_ENTITY_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_EMBEDDED_FIELD_IS_DROPPED = "ROOM_EMBEDDED_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_PARENT_FIELD_IS_DROPPED = "ROOM_PARENT_FIELD_INDEX_IS_DROPPED";
diff --git a/room/room-common/api/restricted_current.txt b/room/room-common/api/restricted_current.txt
index 220d387..722a507 100644
--- a/room/room-common/api/restricted_current.txt
+++ b/room/room-common/api/restricted_current.txt
@@ -224,6 +224,7 @@
field public static final String CANNOT_CREATE_VERIFICATION_DATABASE = "ROOM_CANNOT_CREATE_VERIFICATION_DATABASE";
field public static final String CURSOR_MISMATCH = "ROOM_CURSOR_MISMATCH";
field public static final String DEFAULT_CONSTRUCTOR = "ROOM_DEFAULT_CONSTRUCTOR";
+ field public static final String DOES_NOT_IMPLEMENT_EQUALS_HASHCODE = "ROOM_TYPE_DOES_NOT_IMPLEMENT_EQUALS_HASHCODE";
field public static final String INDEX_FROM_EMBEDDED_ENTITY_IS_DROPPED = "ROOM_EMBEDDED_ENTITY_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_EMBEDDED_FIELD_IS_DROPPED = "ROOM_EMBEDDED_INDEX_IS_DROPPED";
field public static final String INDEX_FROM_PARENT_FIELD_IS_DROPPED = "ROOM_PARENT_FIELD_INDEX_IS_DROPPED";
diff --git a/room/room-common/src/main/java/androidx/room/RoomWarnings.java b/room/room-common/src/main/java/androidx/room/RoomWarnings.java
index 919e42f..df8868b 100644
--- a/room/room-common/src/main/java/androidx/room/RoomWarnings.java
+++ b/room/room-common/src/main/java/androidx/room/RoomWarnings.java
@@ -31,6 +31,13 @@
public static final String CURSOR_MISMATCH = "ROOM_CURSOR_MISMATCH";
/**
+ * The warning dispatched by Room when the object in the provided method's multimap return
+ * type does not implement equals() and hashCode().
+ */
+ public static final String DOES_NOT_IMPLEMENT_EQUALS_HASHCODE =
+ "ROOM_TYPE_DOES_NOT_IMPLEMENT_EQUALS_HASHCODE";
+
+ /**
* Reported when Room cannot verify database queries during compilation due to lack of
* tmp dir access in JVM.
*/
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/DeclarationCollector.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/DeclarationCollector.kt
index af1a65c..b87d357 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/DeclarationCollector.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/DeclarationCollector.kt
@@ -158,7 +158,13 @@
(candidate.enclosingElement as? XTypeElement)?.isInterface() == true ->
false
// accept if not overridden
- else -> existing.none { it.overrides(candidate, xTypeElement) }
+ else -> existing.none {
+ // we might see the same method twice due to diamond inheritance so we need
+ // check for equals in addition to overrides
+ // note that this is OK to check because you cannot implement the same interface
+ // twice with different type parameters (due to erasure).
+ it == candidate || it.overrides(candidate, xTypeElement)
+ }
}
},
getCandidateDeclarations = XTypeElement::getAllMethods,
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/KSDeclarationExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSDeclarationExt.kt
index 7d79231..b6d549f 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSDeclarationExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSDeclarationExt.kt
@@ -16,6 +16,8 @@
package androidx.room.compiler.processing.ksp
+import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticFileMemberContainer
+import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSDeclaration
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
@@ -42,14 +44,33 @@
* Node that this is not necessarily the parent declaration. e.g. when a property is declared in
* a constructor, its containing type is actual two levels up.
*/
+@OptIn(KspExperimental::class)
internal fun KSDeclaration.findEnclosingMemberContainer(
env: KspProcessingEnv
): KspMemberContainer? {
- return findEnclosingAncestorClassDeclaration()?.let {
+ val memberContainer = findEnclosingAncestorClassDeclaration()?.let {
env.wrapClassDeclaration(it)
} ?: this.containingFile?.let {
env.wrapKSFile(it)
}
+ memberContainer?.let {
+ return it
+ }
+ // in compiled files, we may not find it. Try using the binary name
+
+ val ownerJvmClassName = when (this) {
+ is KSPropertyDeclaration -> env.resolver.getOwnerJvmClassName(this)
+ is KSFunctionDeclaration -> env.resolver.getOwnerJvmClassName(this)
+ else -> null
+ } ?: return null
+ // Binary name of a top level type is its canonical name. So we just load it directly by
+ // that value
+ env.findTypeElement(ownerJvmClassName)?.let {
+ return it
+ }
+ // When a top level function/property is compiled, its containing class does not exist in KSP,
+ // neither the file. So instead, we synthesize one
+ return KspSyntheticFileMemberContainer(ownerJvmClassName)
}
private fun KSDeclaration.findEnclosingAncestorClassDeclaration(): KSClassDeclaration? {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSFunctionExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSFunctionExt.kt
index 76e7323..1e646c6 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSFunctionExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSFunctionExt.kt
@@ -17,6 +17,7 @@
package androidx.room.compiler.processing.ksp
import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.util.ISSUE_TRACKER_LINK
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.KSPropertyDeclaration
@@ -43,8 +44,7 @@
else -> error(
"""
Unexpected overridee type for $this ($overridee).
- Please file a bug with steps to reproduce.
- https://issuetracker.google.com/issues/new?component=413107
+ Please file a bug at $ISSUE_TRACKER_LINK.
""".trimIndent()
)
} ?: returnType
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
index dd6e2bd..f8c7623 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
@@ -19,6 +19,7 @@
import androidx.room.compiler.processing.XNullability
import androidx.room.compiler.processing.javac.kotlin.typeNameFromJvmSignature
import androidx.room.compiler.processing.tryBox
+import androidx.room.compiler.processing.util.ISSUE_TRACKER_LINK
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.KSDeclaration
@@ -255,8 +256,8 @@
} catch (ex: NoSuchMethodException) {
throw IllegalStateException(
"""
- Room couldn't find the constructor it is looking for in JavaPoet. Please file a bug at
- https://issuetracker.google.com/issues/new?component=413107
+ Room couldn't find the constructor it is looking for in JavaPoet.
+ Please file a bug at $ISSUE_TRACKER_LINK.
""".trimIndent(),
ex
)
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 cb4ee5f..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,7 +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
@@ -59,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
@@ -76,8 +89,10 @@
val enclosingContainer = declaration.findEnclosingMemberContainer(env)
checkNotNull(enclosingContainer) {
- "XProcessing does not currently support annotations on top level " +
- "functions with KSP. Cannot process $declaration."
+ """
+ Couldn't find the container element for $declaration.
+ Please file a bug at $ISSUE_TRACKER_LINK.
+ """
}
return when {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFiler.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFiler.kt
index ae3b1f7..e2a0758 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFiler.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFiler.kt
@@ -18,6 +18,7 @@
import androidx.room.compiler.processing.XFiler
import androidx.room.compiler.processing.XMessager
+import androidx.room.compiler.processing.util.ISSUE_TRACKER_LINK
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
import com.google.devtools.ksp.symbol.KSFile
@@ -87,8 +88,8 @@
Diagnostic.Kind.WARNING,
"""
No dependencies are reported for $fileName which will prevent
- incremental compilation. Please file a bug at:
- https://issuetracker.google.com/issues/new?component=413107
+ incremental compilation.
+ Please file a bug at $ISSUE_TRACKER_LINK.
""".trimIndent()
)
Dependencies.ALL_FILES
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt
index 46889e0..1726c96 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt
@@ -79,7 +79,7 @@
)
}
- override fun findTypeElement(qName: String): XTypeElement? {
+ override fun findTypeElement(qName: String): KspTypeElement? {
return typeElementStore[qName]
}
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/KspSyntheticFileMemberContainer.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticFileMemberContainer.kt
new file mode 100644
index 0000000..35dc043
--- /dev/null
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticFileMemberContainer.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.room.compiler.processing.ksp.synthetic
+
+import androidx.room.compiler.processing.XAnnotation
+import androidx.room.compiler.processing.XAnnotationBox
+import androidx.room.compiler.processing.XEquality
+import androidx.room.compiler.processing.ksp.KspMemberContainer
+import androidx.room.compiler.processing.ksp.KspType
+import com.google.devtools.ksp.symbol.KSDeclaration
+import com.squareup.javapoet.ClassName
+import kotlin.reflect.KClass
+
+/**
+ * When a top level function/member is compiled, the generated Java class does not exist in KSP.
+ *
+ * This wrapper synthesizes one from the JVM binary name
+ *
+ * https://docs.oracle.com/javase/specs/jls/se7/html/jls-13.html#jls-13.1
+ */
+internal class KspSyntheticFileMemberContainer(
+ private val binaryName: String
+) : KspMemberContainer, XEquality {
+ override val equalityItems: Array<out Any?> by lazy {
+ arrayOf(binaryName)
+ }
+
+ override val type: KspType?
+ get() = null
+
+ override val declaration: KSDeclaration?
+ get() = null
+
+ override val className: ClassName by lazy {
+ val packageName = binaryName.substringBeforeLast(
+ delimiter = '.',
+ missingDelimiterValue = ""
+ )
+ val shortNames = if (packageName == "") {
+ binaryName
+ } else {
+ binaryName.substring(packageName.length + 1)
+ }.split('$')
+ ClassName.get(
+ packageName,
+ shortNames.first(),
+ *shortNames.drop(1).toTypedArray()
+ )
+ }
+
+ override fun kindName(): String {
+ return "synthethic top level file"
+ }
+
+ override val fallbackLocationText: String
+ get() = binaryName
+
+ override val docComment: String?
+ get() = null
+
+ override fun <T : Annotation> getAnnotations(annotation: KClass<T>): List<XAnnotationBox<T>> {
+ return emptyList()
+ }
+
+ override fun getAllAnnotations(): List<XAnnotation> {
+ return emptyList()
+ }
+
+ override fun hasAnnotation(annotation: KClass<out Annotation>): Boolean {
+ return false
+ }
+
+ override fun hasAnnotationWithPackage(pkg: String): Boolean {
+ return false
+ }
+}
\ No newline at end of file
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/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/util/ErrorMessages.kt
similarity index 69%
copy from benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt
copy to room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/util/ErrorMessages.kt
index 88234c7..1386a08 100644
--- a/benchmark/integration-tests/crystalball-experiment/src/androidTest/java/androidx/benchmark/macro/BenchmarkClass.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/util/ErrorMessages.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,9 @@
* limitations under the License.
*/
-package androidx.benchmark.macro
+package androidx.room.compiler.processing.util
-import android.platform.test.rule.DropCachesRule
-
-class BenchmarkClass {
- val rule = DropCachesRule()
-}
+/**
+ * Link to the issue tracker to report bugs.
+ */
+internal val ISSUE_TRACKER_LINK = "https://issuetracker.google.com/issues/new?component=413107"
\ No newline at end of file
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/TopLevelMembersTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TopLevelMembersTest.kt
index 5582ac2..4054f04 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TopLevelMembersTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/TopLevelMembersTest.kt
@@ -23,7 +23,7 @@
import androidx.room.compiler.processing.util.kspProcessingEnv
import androidx.room.compiler.processing.util.kspResolver
import androidx.room.compiler.processing.util.runKspTest
-import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.KSPropertyDeclaration
@@ -61,34 +61,37 @@
sources = listOf(appSrc),
classpath = classpath
) { invocation ->
- // b/188822146
- // TODO add lib package here once Room updates to a version that includes the
- // https://github.com/google/ksp/issues/396 fix (1.5.0-1.0.0-alpha09)
- val declarations = invocation.kspResolver.getDeclarationsFromPackage("app")
- declarations.filterIsInstance<KSFunctionDeclaration>()
- .toList().let { methods ->
- assertThat(methods).hasSize(1)
- methods.forEach { method ->
- val element = KspExecutableElement.create(
- env = invocation.kspProcessingEnv,
- declaration = method
- )
- assertThat(element.containing.isTypeElement()).isFalse()
- assertThat(element.isStatic()).isTrue()
+ listOf("lib", "app").forEach { pkg ->
+ val declarations = invocation.kspResolver.getDeclarationsFromPackage(pkg)
+ declarations.filterIsInstance<KSFunctionDeclaration>()
+ .toList().let { methods ->
+ assertWithMessage(pkg).that(methods).hasSize(1)
+ methods.forEach { method ->
+ val element = KspExecutableElement.create(
+ env = invocation.kspProcessingEnv,
+ declaration = method
+ )
+ assertWithMessage(pkg).that(
+ element.containing.isTypeElement()
+ ).isFalse()
+ assertWithMessage(pkg).that(element.isStatic()).isTrue()
+ }
}
- }
- declarations.filterIsInstance<KSPropertyDeclaration>()
- .toList().let { properties ->
- assertThat(properties).hasSize(2)
- properties.forEach {
- val element = KspFieldElement.create(
- env = invocation.kspProcessingEnv,
- declaration = it
- )
- assertThat(element.containing.isTypeElement()).isFalse()
- assertThat(element.isStatic()).isTrue()
+ declarations.filterIsInstance<KSPropertyDeclaration>()
+ .toList().let { properties ->
+ assertWithMessage(pkg).that(properties).hasSize(2)
+ properties.forEach {
+ val element = KspFieldElement.create(
+ env = invocation.kspProcessingEnv,
+ declaration = it
+ )
+ assertWithMessage(pkg).that(
+ element.containing.isTypeElement()
+ ).isFalse()
+ assertWithMessage(pkg).that(element.isStatic()).isTrue()
+ }
}
- }
+ }
}
}
}
\ No newline at end of file
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/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
index 65055f6..857e85c 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
@@ -686,6 +686,82 @@
}
@Test
+ fun diamondOverride() {
+ fun buildSrc(pkg: String) = Source.kotlin(
+ "Foo.kt",
+ """
+ package $pkg;
+ interface Parent<T> {
+ fun parent(t: T)
+ }
+
+ interface Child1<T> : Parent<T> {
+ fun child1(t: T)
+ }
+
+ interface Child2<T> : Parent<T> {
+ fun child2(t: T)
+ }
+
+ abstract class Subject1 : Child1<String>, Child2<String>, Parent<String>
+ abstract class Subject2 : Child1<String>, Parent<String>
+ abstract class Subject3 : Child1<String>, Parent<String> {
+ abstract override fun parent(t: String)
+ }
+ """.trimIndent()
+ )
+
+ runProcessorTest(
+ sources = listOf(buildSrc("app")),
+ classpath = compileFiles(listOf(buildSrc("lib")))
+ ) { invocation ->
+ listOf("lib", "app").forEach { pkg ->
+ val objectMethodNames = invocation.processingEnv.requireTypeElement(Any::class)
+ .getAllMethods().names()
+
+ fun XMethodElement.signature(
+ owner: XType
+ ): String {
+ val methodType = this.asMemberOf(owner)
+ val params = methodType.parameterTypes.joinToString(",") {
+ it.typeName.toString()
+ }
+ return "$name($params):${returnType.typeName}"
+ }
+
+ fun XTypeElement.allMethodSignatures(): List<String> = getAllMethods().filterNot {
+ it.name in objectMethodNames
+ }.map { it.signature(this.type) }.toList()
+ invocation.processingEnv.requireTypeElement("$pkg.Subject1").let { subject ->
+ assertWithMessage(subject.qualifiedName).that(
+ subject.allMethodSignatures()
+ ).containsExactly(
+ "child1(java.lang.String):void",
+ "child2(java.lang.String):void",
+ "parent(java.lang.String):void",
+ )
+ }
+ invocation.processingEnv.requireTypeElement("$pkg.Subject2").let { subject ->
+ assertWithMessage(subject.qualifiedName).that(
+ subject.allMethodSignatures()
+ ).containsExactly(
+ "child1(java.lang.String):void",
+ "parent(java.lang.String):void",
+ )
+ }
+ invocation.processingEnv.requireTypeElement("$pkg.Subject3").let { subject ->
+ assertWithMessage(subject.qualifiedName).that(
+ subject.allMethodSignatures()
+ ).containsExactly(
+ "child1(java.lang.String):void",
+ "parent(java.lang.String):void",
+ )
+ }
+ }
+ }
+ }
+
+ @Test
fun allMethods() {
val src = Source.kotlin(
"Foo.kt",
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticFileMemberContainerTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticFileMemberContainerTest.kt
new file mode 100644
index 0000000..1ef355a
--- /dev/null
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticFileMemberContainerTest.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.room.compiler.processing.ksp.synthetic
+
+import androidx.room.compiler.processing.ksp.KspFieldElement
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.compileFiles
+import androidx.room.compiler.processing.util.getField
+import androidx.room.compiler.processing.util.kspResolver
+import androidx.room.compiler.processing.util.runKspTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import com.google.devtools.ksp.KspExperimental
+import com.google.devtools.ksp.symbol.KSPropertyDeclaration
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+@OptIn(KspExperimental::class)
+class KspSyntheticFileMemberContainerTest {
+ @Test
+ fun topLevel_noPackage() {
+ val annotation = Source.kotlin(
+ "MyAnnotation.kt",
+ """
+ annotation class MyAnnotation
+ """.trimIndent()
+ )
+ val appSrc = Source.kotlin(
+ "App.kt",
+ """
+ @MyAnnotation
+ val appMember = 1
+ """.trimIndent()
+ )
+ runKspTest(
+ sources = listOf(annotation, appSrc)
+ ) { invocation ->
+ val elements = invocation.kspResolver.getSymbolsWithAnnotation("MyAnnotation").toList()
+ assertThat(elements).hasSize(1)
+ val className = elements.map {
+ val owner = invocation.kspResolver.getOwnerJvmClassName(it as KSPropertyDeclaration)
+ assertWithMessage(it.toString()).that(owner).isNotNull()
+ KspSyntheticFileMemberContainer(owner!!).className
+ }.first()
+ assertThat(className.packageName()).isEmpty()
+ assertThat(className.simpleNames()).containsExactly("AppKt")
+ }
+ }
+
+ @Test
+ fun nestedClassNames() {
+ fun buildSources(pkg: String) = listOf(
+ Source.java(
+ "$pkg.JavaClass",
+ """
+ package $pkg;
+ public class JavaClass {
+ int member;
+ public static class NestedClass {
+ int member;
+ }
+ public class InnerClass {
+ int member;
+ }
+ }
+ """.trimIndent()
+ ),
+ Source.java(
+ "${pkg}JavaClass",
+ """
+ public class ${pkg}JavaClass {
+ int member;
+ public static class NestedClass {
+ int member;
+ }
+ public class InnerClass {
+ int member;
+ }
+ }
+ """.trimIndent()
+ ),
+ Source.kotlin(
+ "$pkg/KotlinClass.kt",
+ """
+ package $pkg
+ class KotlinClass {
+ val member = 1
+ class NestedClass {
+ val member = 1
+ }
+ inner class InnerClass {
+ val member = 1
+ }
+ }
+ """.trimIndent()
+ ),
+ Source.kotlin(
+ "KotlinClass.kt",
+ """
+ class ${pkg}KotlinClass {
+ val member = 1
+ class NestedClass {
+ val member = 1
+ }
+ inner class InnerClass {
+ val member = 1
+ }
+ }
+ """.trimIndent()
+ )
+ )
+ val lib = compileFiles(buildSources("lib"))
+ runKspTest(
+ sources = buildSources("app"),
+ classpath = lib
+ ) { invocation ->
+ fun runTest(qName: String) {
+ invocation.processingEnv.requireTypeElement(qName).let { target ->
+ val field = target.getField("member") as KspFieldElement
+ val owner = invocation.kspResolver.getOwnerJvmClassName(field.declaration)
+ assertWithMessage(qName).that(owner).isNotNull()
+ val synthetic = KspSyntheticFileMemberContainer(owner!!)
+ assertWithMessage(qName).that(target.className).isEqualTo(synthetic.className)
+ }
+ }
+ listOf("lib", "app").forEach { pkg ->
+ // test both top level and in package cases
+ listOf(pkg, "$pkg.").forEach { prefix ->
+ runTest("${prefix}JavaClass")
+ runTest("${prefix}JavaClass.NestedClass")
+ runTest("${prefix}JavaClass.InnerClass")
+ runTest("${prefix}KotlinClass")
+ runTest("${prefix}KotlinClass.NestedClass")
+ runTest("${prefix}KotlinClass.InnerClass")
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/xtype_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/xtype_ext.kt
index cc94880..f003876 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/xtype_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/xtype_ext.kt
@@ -21,6 +21,8 @@
import androidx.room.compiler.processing.isKotlinUnit
import androidx.room.compiler.processing.isVoid
import androidx.room.compiler.processing.isVoidObject
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.TypeName
/**
* Returns `true` if this type is not the `void` type.
@@ -51,3 +53,36 @@
* Returns `true` if this is not `byte` type.
*/
fun XType.isNotByte() = !isByte()
+
+/**
+ * Checks if the class of the provided type has the equals() and hashCode() methods declared.
+ * If they are not found at the current class level, the method recursively moves on to the
+ * super class level and continues to look for these declared methods.
+ */
+fun XType.implementsEqualsAndHashcode(): Boolean {
+ if (this.typeName.isPrimitive || this.typeName.isBoxedPrimitive) {
+ return true
+ }
+ val typeElement = this.typeElement ?: return false
+
+ if (typeElement.className == ClassName.OBJECT) {
+ return false
+ }
+
+ val hasEquals = typeElement.getDeclaredMethods().any {
+ it.name == "equals" &&
+ it.returnType.typeName == TypeName.BOOLEAN &&
+ it.parameters.count() == 1 &&
+ it.parameters[0].type.typeName == TypeName.OBJECT
+ }
+ val hasHashCode = typeElement.getDeclaredMethods().any {
+ it.name == "hashCode" &&
+ it.returnType.typeName == TypeName.INT &&
+ it.parameters.count() == 0
+ }
+
+ if (hasEquals && hasHashCode) {
+ return true
+ }
+ return typeElement.superType?.let { it.implementsEqualsAndHashcode() } ?: false
+}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index c9b955e..656b9e8 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -67,6 +67,10 @@
val AUTO_INCREMENT_EMBEDDED_HAS_MULTIPLE_FIELDS = "When @PrimaryKey annotation is used on a" +
" field annotated with @Embedded, the embedded class should have only 1 field."
+ val DO_NOT_USE_GENERIC_IMMUTABLE_MULTIMAP = "Do not use ImmutableMultimap as a type (as with" +
+ " Multimap itself). Instead use the subtypes such as ImmutableSetMultimap or " +
+ "ImmutableListMultimap."
+
fun multiplePrimaryKeyAnnotations(primaryKeys: List<String>): String {
return """
You cannot have multiple primary keys defined in an Entity. If you
@@ -128,6 +132,10 @@
fun cannotFindQueryResultAdapter(returnTypeName: TypeName) = "Not sure how to convert a " +
"Cursor to this method's return type ($returnTypeName)."
+ fun classMustImplementEqualsAndHashCode(mapType: TypeName, keyType: TypeName) = "The key" +
+ " of the provided method's multimap return type ($mapType) must implement equals() and " +
+ "hashCode(). Key type is: $keyType."
+
val INSERTION_DOES_NOT_HAVE_ANY_PARAMETERS_TO_INSERT = "Method annotated with" +
" @Insert but does not have any parameters to insert."
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index 36df772..ae7a141 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -21,6 +21,7 @@
import androidx.room.compiler.processing.isEnum
import androidx.room.ext.CommonTypeNames
import androidx.room.ext.GuavaBaseTypeNames
+import androidx.room.ext.implementsEqualsAndHashcode
import androidx.room.ext.isEntityElement
import androidx.room.ext.isNotByte
import androidx.room.ext.isNotKotlinUnit
@@ -32,6 +33,8 @@
import androidx.room.processor.EntityProcessor
import androidx.room.processor.FieldProcessor
import androidx.room.processor.PojoProcessor
+import androidx.room.processor.ProcessorErrors.DO_NOT_USE_GENERIC_IMMUTABLE_MULTIMAP
+import androidx.room.processor.ProcessorErrors.classMustImplementEqualsAndHashCode
import androidx.room.solver.binderprovider.CoroutineFlowResultBinderProvider
import androidx.room.solver.binderprovider.CursorQueryResultBinderProvider
import androidx.room.solver.binderprovider.DataSourceFactoryQueryResultBinderProvider
@@ -54,8 +57,10 @@
import androidx.room.solver.query.parameter.QueryParameterAdapter
import androidx.room.solver.query.result.ArrayQueryResultAdapter
import androidx.room.solver.query.result.EntityRowAdapter
+import androidx.room.solver.query.result.GuavaImmutableMultimapQueryResultAdapter
import androidx.room.solver.query.result.GuavaOptionalQueryResultAdapter
import androidx.room.solver.query.result.ImmutableListQueryResultAdapter
+import androidx.room.solver.query.result.ImmutableMapQueryResultAdapter
import androidx.room.solver.query.result.ListQueryResultAdapter
import androidx.room.solver.query.result.MapQueryResultAdapter
import androidx.room.solver.query.result.OptionalQueryResultAdapter
@@ -93,8 +98,14 @@
import androidx.room.solver.types.StringColumnTypeAdapter
import androidx.room.solver.types.TypeConverter
import androidx.room.vo.ShortcutQueryParameter
+import androidx.room.vo.Warning
import com.google.common.annotations.VisibleForTesting
import com.google.common.collect.ImmutableList
+import com.google.common.collect.ImmutableListMultimap
+import com.google.common.collect.ImmutableMultimap
+import com.google.common.collect.ImmutableSetMultimap
+import com.google.common.collect.ImmutableMap
+import com.squareup.javapoet.ClassName
import java.util.LinkedList
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
@@ -462,10 +473,57 @@
typeArg = typeArg,
rowAdapter = rowAdapter
)
- } else if (typeMirror.isTypeOf(java.util.Map::class)) {
- // TODO: Handle nested collection values in the map
- // TODO: Verify that hashCode() and equals() are declared by the keyTypeArg
+ } else if (typeMirror.isTypeOf(ImmutableMap::class)) {
+ val keyTypeArg = typeMirror.typeArguments[0].extendsBoundOrSelf()
+ val valueTypeArg = typeMirror.typeArguments[1].extendsBoundOrSelf()
+ // Create a type mirror for a regular Map in order to use MapQueryResultAdapter. This
+ // avoids code duplication as Immutable Map can be initialized by creating an immutable
+ // copy of a regular map.
+ val mapType = context.processingEnv.getDeclaredType(
+ context.processingEnv.requireTypeElement(Map::class),
+ keyTypeArg,
+ valueTypeArg
+ )
+ val resultAdapter = findQueryResultAdapter(mapType, query = query) ?: return null
+ return ImmutableMapQueryResultAdapter(
+ keyTypeArg = keyTypeArg,
+ valueTypeArg = valueTypeArg,
+ resultAdapter = resultAdapter
+ )
+ } else if (typeMirror.isTypeOf(ImmutableSetMultimap::class) ||
+ typeMirror.isTypeOf(ImmutableListMultimap::class) ||
+ typeMirror.isTypeOf(ImmutableMultimap::class)
+ ) {
+ val keyTypeArg = typeMirror.typeArguments[0].extendsBoundOrSelf()
+ val valueTypeArg = typeMirror.typeArguments[1].extendsBoundOrSelf()
+
+ if (valueTypeArg.typeElement == null) {
+ context.logger.e(
+ "Guava multimap 'value' type argument does not represent a class. " +
+ "Found $valueTypeArg."
+ )
+ return null
+ }
+
+ val immutableClassName = if (typeMirror.isTypeOf(ImmutableListMultimap::class)) {
+ ClassName.get(ImmutableListMultimap::class.java)
+ } else if (typeMirror.isTypeOf(ImmutableSetMultimap::class)) {
+ ClassName.get(ImmutableSetMultimap::class.java)
+ } else {
+ // Return type is base class ImmutableMultimap which is not recommended.
+ context.logger.e(DO_NOT_USE_GENERIC_IMMUTABLE_MULTIMAP)
+ return null
+ }
+
+ return GuavaImmutableMultimapQueryResultAdapter(
+ keyTypeArg = keyTypeArg,
+ valueTypeArg = valueTypeArg,
+ keyRowAdapter = findRowAdapter(keyTypeArg, query) ?: return null,
+ valueRowAdapter = findRowAdapter(valueTypeArg, query) ?: return null,
+ immutableClassName = immutableClassName
+ )
+ } else if (typeMirror.isTypeOf(java.util.Map::class)) {
val keyTypeArg = typeMirror.typeArguments[0].extendsBoundOrSelf()
val mapValueTypeArg = typeMirror.typeArguments[1].extendsBoundOrSelf()
@@ -476,9 +534,19 @@
)
return null
}
+ // TODO: Handle nested collection values in the map
+ if (!keyTypeArg.implementsEqualsAndHashcode()) {
+ context.logger.w(
+ Warning.DOES_NOT_IMPLEMENT_EQUALS_HASHCODE,
+ keyTypeArg.typeElement,
+ classMustImplementEqualsAndHashCode(
+ typeMirror.typeName,
+ keyTypeArg.typeName
+ )
+ )
+ }
val collectionTypeRaw = context.COMMON_TYPES.READONLY_COLLECTION.rawType
-
if (collectionTypeRaw.isAssignableFrom(mapValueTypeArg.rawType)) {
// The Map's value type argument is assignable to a Collection, we need to make
// sure it is either a list or a set.
@@ -486,8 +554,7 @@
mapValueTypeArg.isTypeOf(java.util.List::class) ||
mapValueTypeArg.isTypeOf(java.util.Set::class)
) {
- val valueTypeArg =
- mapValueTypeArg.typeArguments.single().extendsBoundOrSelf()
+ val valueTypeArg = mapValueTypeArg.typeArguments.single().extendsBoundOrSelf()
return MapQueryResultAdapter(
keyTypeArg = keyTypeArg,
valueTypeArg = valueTypeArg,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt
new file mode 100644
index 0000000..612d276
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.room.solver.query.result
+
+import androidx.room.compiler.processing.XType
+import androidx.room.ext.L
+import androidx.room.ext.T
+import androidx.room.solver.CodeGenScope
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.ParameterizedTypeName
+
+class GuavaImmutableMultimapQueryResultAdapter(
+ private val keyTypeArg: XType,
+ private val valueTypeArg: XType,
+ private val keyRowAdapter: RowAdapter,
+ private val valueRowAdapter: RowAdapter,
+ private val immutableClassName: ClassName
+) : QueryResultAdapter(listOf(keyRowAdapter, valueRowAdapter)) {
+ private val mapType = ParameterizedTypeName.get(
+ immutableClassName,
+ keyTypeArg.typeName,
+ valueTypeArg.typeName
+ )
+
+ override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
+ val mapVarName = scope.getTmpVar("_mapBuilder")
+
+ scope.builder().apply {
+ keyRowAdapter.onCursorReady(cursorVarName, scope)
+ valueRowAdapter.onCursorReady(cursorVarName, scope)
+ addStatement(
+ "final $T.Builder<$T, $T> $L = $T.builder()",
+ immutableClassName,
+ keyTypeArg.typeName,
+ valueTypeArg.typeName,
+ mapVarName,
+ immutableClassName
+ )
+ val tmpKeyVarName = scope.getTmpVar("_key")
+ val tmpValueVarName = scope.getTmpVar("_value")
+ beginControlFlow("while ($L.moveToNext())", cursorVarName).apply {
+ addStatement("final $T $L", keyTypeArg.typeName, tmpKeyVarName)
+ keyRowAdapter.convert(tmpKeyVarName, cursorVarName, scope)
+ addStatement("final $T $L", valueTypeArg.typeName, tmpValueVarName)
+ valueRowAdapter.convert(tmpValueVarName, cursorVarName, scope)
+ addStatement("$L.put($L, $L)", mapVarName, tmpKeyVarName, tmpValueVarName)
+ }
+ endControlFlow()
+ addStatement("final $T $L = $L.build()", mapType, outVarName, mapVarName)
+ keyRowAdapter.onCursorFinished()?.invoke(scope)
+ valueRowAdapter.onCursorFinished()?.invoke(scope)
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableMapQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableMapQueryResultAdapter.kt
new file mode 100644
index 0000000..c28b189
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableMapQueryResultAdapter.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.room.solver.query.result
+
+import androidx.room.compiler.processing.XType
+import androidx.room.ext.L
+import androidx.room.ext.T
+import androidx.room.solver.CodeGenScope
+import com.google.common.collect.ImmutableMap
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.ParameterizedTypeName
+
+class ImmutableMapQueryResultAdapter(
+ private val keyTypeArg: XType,
+ private val valueTypeArg: XType,
+ private val resultAdapter: QueryResultAdapter
+) : QueryResultAdapter(resultAdapter.rowAdapters) {
+ override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
+ scope.builder().apply {
+ val mapVarName = scope.getTmpVar("_mapResult")
+ resultAdapter.convert(mapVarName, cursorVarName, scope)
+ addStatement(
+ "final $T $L = $T.copyOf($L)",
+ ParameterizedTypeName.get(
+ ClassName.get(ImmutableMap::class.java),
+ keyTypeArg.typeName,
+ valueTypeArg.typeName
+ ),
+ outVarName,
+ ClassName.get(ImmutableMap::class.java),
+ mapVarName
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/vo/Warning.kt b/room/room-compiler/src/main/kotlin/androidx/room/vo/Warning.kt
index e825f29..fc3bd6a 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/vo/Warning.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/vo/Warning.kt
@@ -25,6 +25,7 @@
enum class Warning(val publicKey: String) {
ALL("ALL"),
CURSOR_MISMATCH("ROOM_CURSOR_MISMATCH"),
+ DOES_NOT_IMPLEMENT_EQUALS_HASHCODE("ROOM_TYPE_DOES_NOT_IMPLEMENT_EQUALS_HASHCODE"),
MISSING_JAVA_TMP_DIR("ROOM_MISSING_JAVA_TMP_DIR"),
CANNOT_CREATE_VERIFICATION_DATABASE("ROOM_CANNOT_CREATE_VERIFICATION_DATABASE"),
PRIMARY_KEY_FROM_EMBEDDED_IS_DROPPED("ROOM_EMBEDDED_PRIMARY_KEY_IS_DROPPED"),
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/BaseDaoTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/BaseDaoTest.kt
index 148e40d..a87d4b5 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/BaseDaoTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/BaseDaoTest.kt
@@ -161,6 +161,69 @@
}
}
+ @Test
+ fun extendSameInterfaceTwice() {
+ val source = Source.kotlin(
+ "Foo.kt",
+ """
+ import androidx.room.*
+
+ interface Parent<T> {
+ @Delete
+ fun delete(t: T)
+ }
+
+ interface Child1<T> : Parent<T> {
+ @Insert
+ fun insert(t: T)
+ }
+
+ interface Child2<T> : Parent<T> {
+ @Update
+ fun update(t: T)
+ }
+
+ @Entity
+ data class Data(
+ @PrimaryKey(autoGenerate = false)
+ val id: Long,
+ val data: String
+ )
+
+ @Dao
+ abstract class Dao1 : Child1<Data>, Child2<Data>, Parent<Data>
+
+ @Dao
+ abstract class Dao2 : Child1<Data>, Parent<Data>
+
+ @Dao
+ abstract class Dao3 : Child1<Data>, Parent<Data> {
+ @Delete
+ abstract override fun delete(t: Data)
+ }
+
+ abstract class MyDb : RoomDatabase() {
+ }
+ """.trimIndent()
+ )
+ runProcessorTest(
+ sources = listOf(source)
+ ) { invocation ->
+ val dbElm = invocation.context.processingEnv
+ .requireTypeElement("MyDb")
+ val dbType = dbElm.type
+ // if we could create valid code, it is good, no need for assertions.
+ listOf("Dao1", "Dao2", "Dao3").forEach { name ->
+ val dao = invocation.processingEnv.requireTypeElement(name)
+ val processed = DaoProcessor(
+ invocation.context, dao, dbType, null
+ ).process()
+ DaoWriter(processed, dbElm, invocation.processingEnv)
+ .write(invocation.processingEnv)
+ }
+ }
+ }
+
fun baseDao(code: String, handler: (Dao) -> Unit) {
val baseClass = Source.java(
"foo.bar.BaseDao",
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
index 63722a4..3e9bbfd 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
@@ -31,6 +31,7 @@
import androidx.room.ext.typeName
import androidx.room.parser.QueryType
import androidx.room.parser.Table
+import androidx.room.processor.ProcessorErrors.DO_NOT_USE_GENERIC_IMMUTABLE_MULTIMAP
import androidx.room.processor.ProcessorErrors.cannotFindQueryResultAdapter
import androidx.room.solver.query.result.DataSourceFactoryQueryResultBinder
import androidx.room.solver.query.result.ListQueryResultAdapter
@@ -987,6 +988,18 @@
"""
public static class Username {
public String name;
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Username username = (Username) o;
+ if (name != username.name) return false;
+ return true;
+ }
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
}
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM User JOIN Relation ON (User.uid = Relation.userId)")
@@ -1341,4 +1354,21 @@
}
}
}
+
+ @Test
+ fun testInvalidGenericMultimapJoin() {
+ singleQueryMethod<ReadQueryMethod>(
+ """
+ @Query("select * from User u JOIN Book b ON u.uid == b.uid")
+ abstract com.google.common.collect.ImmutableMultimap<User, Book>
+ getInvalidCollectionMultimap();
+ """
+ ) { _, invocation ->
+ invocation.assertCompilationResult {
+ hasErrorCount(2)
+ hasError(DO_NOT_USE_GENERIC_IMMUTABLE_MULTIMAP)
+ hasErrorContaining("Not sure how to convert a Cursor to this method's return type")
+ }
+ }
+ }
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
index 4b7ebf6..e9c4b7f 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
@@ -35,6 +35,7 @@
import androidx.room.ext.RxJava2TypeNames
import androidx.room.ext.RxJava3TypeNames
import androidx.room.ext.T
+import androidx.room.ext.implementsEqualsAndHashcode
import androidx.room.parser.SQLTypeAffinity
import androidx.room.processor.Context
import androidx.room.processor.CustomConverterProcessor
@@ -1139,6 +1140,152 @@
}
}
+ @Test
+ fun testEqualsAndHashcodeImplemented() {
+ val classExtendsClassWithEqualsAndHashcodeFunctions = Source.java(
+ "foo.bar.Human",
+ """
+ package foo.bar;
+ public class Human extends Username {
+ public String relationId;
+ }
+ """.trimIndent()
+ )
+ val classWithFncs = Source.java(
+ "foo.bar.Username",
+ """
+ package foo.bar;
+ public class Username extends Person {
+ public String name;
+ @Override
+ public boolean equals(Object o) {
+ return false;
+ }
+ @Override
+ public int hashCode() {
+ return 0;
+ }
+ }
+ """.trimIndent()
+ )
+ val classWithoutFncs = Source.java(
+ "foo.bar.Person",
+ """
+ package foo.bar;
+ public class Person {
+ public String userId;
+ }
+ """.trimIndent()
+ )
+ val enumClass = Source.java(
+ "foo.bar.Names",
+ """
+ package foo.bar;
+ public enum Names {
+ ELLA,
+ BOB,
+ JAMES
+ }
+ """.trimIndent()
+ )
+ val classWithWrongFncs = Source.java(
+ "foo.bar.UsernameWithWrongFncs",
+ """
+ package foo.bar;
+ public class UsernameWithWrongFncs {
+ public String name;
+ public boolean equals() {
+ return true;
+ }
+ public int hashCode(int num) {
+ return num;
+ }
+ }
+ """.trimIndent()
+ )
+ runProcessorTest(
+ sources = listOf(
+ classExtendsClassWithEqualsAndHashcodeFunctions,
+ classWithFncs,
+ classWithoutFncs,
+ enumClass,
+ classWithWrongFncs
+ )
+ ) { invocation ->
+ val enumCase = invocation.processingEnv.requireTypeElement("foo.bar.Names")
+ val inheritedCase = invocation.processingEnv.requireTypeElement("foo.bar.Human")
+ val wrongFunctionsCase = invocation.processingEnv.requireTypeElement(
+ "foo.bar.UsernameWithWrongFncs"
+ )
+ val noEqualsOrHashcodeCase = invocation.processingEnv.requireTypeElement(
+ "foo.bar.Person"
+ )
+ assertThat(enumCase.type.implementsEqualsAndHashcode()).isTrue()
+ assertThat(inheritedCase.type.implementsEqualsAndHashcode()).isTrue()
+ assertThat(wrongFunctionsCase.type.implementsEqualsAndHashcode()).isFalse()
+ assertThat(noEqualsOrHashcodeCase.type.implementsEqualsAndHashcode()).isFalse()
+ }
+ }
+
+ @Test
+ fun testEqualsAndHashcodeCheckWithJavaPrimitive() {
+ val inputSource = Source.java(
+ "foo.bar.Subject",
+ """
+ package foo.bar;
+ public class Subject {
+ public int primitiveInt = 0;
+ public Integer boxedInt = 1;
+ public boolean primitiveBool = true;
+ public Boolean boxedBool = false;
+ public double primitiveDouble = 2.2;
+ public Double boxedDouble = 3.3;
+ public long primitiveLong = 4L;
+ public Long boxedLong = 5L;
+ }
+ """.trimIndent()
+ )
+ runProcessorTest(
+ sources = listOf(
+ inputSource,
+ COMMON.USER,
+ COMMON.PAGING_SOURCE,
+ COMMON.LIMIT_OFFSET_PAGING_SOURCE,
+ ),
+ ) { invocation ->
+ val subjectTypeElement =
+ invocation.processingEnv.requireTypeElement("foo.bar.Subject")
+ subjectTypeElement.getAllFieldsIncludingPrivateSupers().forEach { field ->
+ assertThat(field.type.implementsEqualsAndHashcode()).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testEqualsAndHashcodeCheckWithKotlinPrimitive() {
+ val source = Source.kotlin(
+ "Foo.kt",
+ """
+ import androidx.room.*
+ class Subject {
+ val anInteger = 0
+ val aBoolean = true
+ val aDouble = 2.2
+ val aLong = 5L
+ }
+ """.trimIndent()
+ )
+ runProcessorTest(
+ sources = listOf(source)
+ ) { invocation ->
+ val subjectTypeElement = invocation.processingEnv.requireTypeElement("Subject")
+
+ subjectTypeElement.getDeclaredFields().forEach {
+ assertThat(it.type.implementsEqualsAndHashcode()).isTrue()
+ }
+ }
+ }
+
private fun createIntListToStringBinders(invocation: XTestInvocation): List<TypeConverter> {
val intType = invocation.processingEnv.requireType(Integer::class)
val listElement = invocation.processingEnv.requireTypeElement(java.util.List::class)
diff --git a/room/room-paging/build.gradle b/room/room-paging/build.gradle
index 2d3d307..0c4e5dc 100644
--- a/room/room-paging/build.gradle
+++ b/room/room-paging/build.gradle
@@ -29,7 +29,7 @@
// Note that the CI does not run tests with KSP yet so this is only for local usage.
// Once variants are properly supported by both ksp and AndroidX, we'll add support for this.
// (b/153917176)
-def useKsp = project.properties.getOrDefault("useKsp", "true").toBoolean()
+def useKsp = project.properties.getOrDefault("useKsp", "false").toBoolean()
if (useKsp) {
apply plugin: "com.google.devtools.ksp"
} else {
@@ -45,9 +45,9 @@
dependencies {
api(libs.kotlinStdlib)
- implementation("androidx.paging:paging-common:3.0.0")
- implementation project(":room:room-runtime")
+ implementation(project(":room:room-runtime"))
implementation(project(":room:room-ktx"))
+ implementation(projectOrArtifact(":paging:paging-common"))
androidTestImplementation(libs.kotlinCoroutinesTest)
androidTestImplementation(libs.multidex)
diff --git a/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt b/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
index e96da43..0954498 100644
--- a/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
+++ b/room/room-paging/src/androidTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
@@ -21,6 +21,7 @@
import androidx.paging.LoadType
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
+import androidx.paging.PagingSource.LoadResult
import androidx.paging.PagingState
import androidx.room.Room
import androidx.room.RoomDatabase
@@ -150,28 +151,42 @@
@Test
fun load_initialLoad() {
val pagingSource = LimitOffsetPagingSourceImpl(database)
+ dao.addAllItems(itemsList)
runBlocking {
- // test empty load
- val result = pagingSource.refresh()
+ val result = pagingSource.refresh() as LoadResult.Page
- assertTrue(result.data.isEmpty())
- // now add data
- dao.addAllItems(itemsList)
- val result2 = pagingSource.refresh()
-
- assertThat(result2.data).containsExactlyElementsIn(
+ assertThat(result.data).containsExactlyElementsIn(
itemsList.subList(0, 15)
)
}
}
@Test
+ fun load_initialEmptyLoad() {
+ val pagingSource = LimitOffsetPagingSourceImpl(database)
+ runBlocking {
+ val result = pagingSource.refresh() as LoadResult.Page
+
+ assertTrue(result.data.isEmpty())
+
+ // now add items
+ dao.addAllItems(itemsList)
+
+ // the db write should cause pagingSource to realize it is invalid
+ assertThat(pagingSource.refresh()).isInstanceOf(
+ LoadResult.Invalid::class.java
+ )
+ assertTrue(pagingSource.invalid)
+ }
+ }
+
+ @Test
fun load_initialLoadWithInitialKey() {
dao.addAllItems(itemsList)
val pagingSource = LimitOffsetPagingSourceImpl(database)
// refresh with initial key = 20
runBlocking {
- val result = pagingSource.refresh(key = 20)
+ val result = pagingSource.refresh(key = 20) as LoadResult.Page
// item in pos 21-35 (TestItemId 20-34) loaded
assertThat(result.data).containsExactlyElementsIn(
@@ -188,7 +203,7 @@
queryString = "SELECT * FROM $tableName ORDER BY id ASC LIMIT 10 OFFSET 30",
)
runBlocking {
- val result = pagingSource.refresh()
+ val result = pagingSource.refresh() as LoadResult.Page
// default initial loadSize = 15 starting from index 0.
// user supplied limit offset should cause initial loadSize = 10, starting from index 30
@@ -213,7 +228,7 @@
)
// refresh with initial key = 40
runBlocking {
- val result = pagingSource.refresh(key = 40)
+ val result = pagingSource.refresh(key = 40) as LoadResult.Page
// initial loadSize = 15, but limited by id < 50, should only load items 40 - 50
assertThat(result.data).containsExactlyElementsIn(
@@ -236,7 +251,7 @@
"ORDER BY id ASC",
)
runBlocking {
- val result = pagingSource.refresh()
+ val result = pagingSource.refresh() as LoadResult.Page
assertThat(result.data).containsExactly(itemsList[90])
assertThat(pagingSource.itemCount.get()).isEqualTo(1)
@@ -251,7 +266,7 @@
queryString = "SELECT * FROM $tableName ORDER BY id ASC LIMIT 10 OFFSET 500",
)
runBlocking {
- val result = pagingSource.refresh()
+ val result = pagingSource.refresh() as LoadResult.Page
// invalid OFFSET = 500 should return empty data
assertThat(result.data).isEmpty()
@@ -273,7 +288,7 @@
queryString = "SELECT * FROM $tableName ORDER BY id ASC LIMIT -1",
)
runBlocking {
- val result = pagingSource.refresh()
+ val result = pagingSource.refresh() as LoadResult.Page
// ensure that it respects SQLite's default behavior for negative LIMIT
assertThat(result.data).containsExactlyElementsIn(
@@ -292,18 +307,18 @@
fun invalidInitialKey_dbEmpty_returnsEmpty() {
val pagingSource = LimitOffsetPagingSourceImpl(database)
runBlocking {
- val result = pagingSource.refresh(key = 101)
+ val result = pagingSource.refresh(key = 101) as LoadResult.Page
assertThat(result.data).isEmpty()
}
}
@Test
- fun invalidInitialKey_keyTooLarge_returnsEmpty() {
+ fun invalidInitialKey_keyTooLarge_returnsLastPage() {
val pagingSource = LimitOffsetPagingSourceImpl(database)
dao.addAllItems(itemsList)
runBlocking {
- val result = pagingSource.refresh(key = 101)
+ val result = pagingSource.refresh(key = 101) as LoadResult.Page
// should load the last page
assertThat(result.data).containsExactlyElementsIn(
@@ -335,7 +350,7 @@
// to bypass check for initial load and run as non-initial load
pagingSource.itemCount.set(100)
runBlocking {
- val result = pagingSource.append(key = 20)
+ val result = pagingSource.append(key = 20) as LoadResult.Page
// item in pos 21-25 (TestItemId 20-24) loaded
assertThat(result.data).containsExactlyElementsIn(
@@ -353,7 +368,7 @@
// to bypass check for initial load and run as non-initial load
pagingSource.itemCount.set(100)
runBlocking {
- val result = pagingSource.append(key = 97)
+ val result = pagingSource.append(key = 97) as LoadResult.Page
// item in pos 98-100 (TestItemId 97-99) loaded
assertThat(result.data).containsExactlyElementsIn(
@@ -371,15 +386,15 @@
// to bypass check for initial load and run as non-initial load
pagingSource.itemCount.set(100)
runBlocking {
- // first prepend
- val result = pagingSource.append(key = 30)
+ // first append
+ val result = pagingSource.append(key = 30) as LoadResult.Page
// TestItemId 30-34 loaded
assertThat(result.data).containsExactlyElementsIn(
itemsList.subList(30, 35)
)
- // second prepend using nextKey from previous load
- val result2 = pagingSource.append(key = result.nextKey)
+ // second append using nextKey from previous load
+ val result2 = pagingSource.append(key = result.nextKey) as LoadResult.Page
// TestItemId 35 - 39 loaded
assertThat(result2.data).containsExactlyElementsIn(
@@ -389,13 +404,39 @@
}
@Test
+ fun append_invalidResult() {
+ val pagingSource = LimitOffsetPagingSourceImpl(database)
+ dao.addAllItems(itemsList)
+ // to bypass check for initial load and run as non-initial load
+ pagingSource.itemCount.set(100)
+ runBlocking {
+ // first append
+ val result = pagingSource.append(key = 30) as LoadResult.Page
+
+ // TestItemId 30-34 loaded
+ assertThat(result.data).containsExactlyElementsIn(
+ itemsList.subList(30, 35)
+ )
+
+ // make changes to database
+ dao.deleteTestItem(itemsList[42])
+
+ // this append should check invalidation tables, realize it has been updated,
+ // and return a LoadResult.Invalid
+ val result2 = pagingSource.append(key = result.nextKey)
+
+ assertThat(result2).isInstanceOf(LoadResult.Invalid::class.java)
+ }
+ }
+
+ @Test
fun prepend_middleOfList() {
val pagingSource = LimitOffsetPagingSourceImpl(database)
dao.addAllItems(itemsList)
// to bypass check for initial load and run as non-initial load
pagingSource.itemCount.set(100)
runBlocking {
- val result = pagingSource.prepend(key = 30)
+ val result = pagingSource.prepend(key = 30) as LoadResult.Page
assertThat(result.data).containsExactlyElementsIn(
itemsList.subList(25, 30)
@@ -412,7 +453,7 @@
// to bypass check for initial load and run as non-initial load
pagingSource.itemCount.set(100)
runBlocking {
- val result = pagingSource.prepend(key = 3)
+ val result = pagingSource.prepend(key = 3) as LoadResult.Page
// items in pos 0 - 2 (TestItemId 0 - 2) loaded
assertThat(result.data).containsExactlyElementsIn(
@@ -431,14 +472,14 @@
pagingSource.itemCount.set(100)
runBlocking {
// first prepend
- val result = pagingSource.prepend(key = 20)
+ val result = pagingSource.prepend(key = 20) as LoadResult.Page
// items pos 16-20 (TestItemId 15-19) loaded
assertThat(result.data).containsExactlyElementsIn(
itemsList.subList(15, 20)
)
// second prepend using prevKey from previous load
- val result2 = pagingSource.prepend(key = result.prevKey)
+ val result2 = pagingSource.prepend(key = result.prevKey) as LoadResult.Page
// items pos 11-15 (TestItemId 10 - 14) loaded
assertThat(result2.data).containsExactlyElementsIn(
@@ -448,24 +489,51 @@
}
@Test
+ fun prepend_invalidResult() {
+ val pagingSource = LimitOffsetPagingSourceImpl(database)
+ dao.addAllItems(itemsList)
+ // to bypass check for initial load and run as non-initial load
+ pagingSource.itemCount.set(100)
+ runBlocking {
+ // first prepend
+ val result = pagingSource.prepend(key = 20) as LoadResult.Page
+
+ // items pos 16-20 (TestItemId 15-19) loaded
+ assertThat(result.data).containsExactlyElementsIn(
+ itemsList.subList(15, 20)
+ )
+
+ // now write into database
+ dao.deleteTestItem(itemsList[30])
+
+ // second prepend using prevKey from previous load
+ val result2 = pagingSource.prepend(key = result.prevKey)
+
+ // this prepend should check invalidation tables, realize it has been updated,
+ // and return a LoadResult.Invalid
+ assertThat(result2).isInstanceOf(LoadResult.Invalid::class.java)
+ }
+ }
+
+ @Test
fun test_itemsBefore() {
val pagingSource = LimitOffsetPagingSourceImpl(database)
dao.addAllItems(itemsList)
runBlocking {
// for initial load
- val result = pagingSource.refresh(key = 50)
+ val result = pagingSource.refresh(key = 50) as LoadResult.Page
// initial loads items in pos 51 - 65, should have 50 items before
assertThat(result.itemsBefore).isEqualTo(50)
// prepend from initial load
- val result2 = pagingSource.prepend(key = result.prevKey)
+ val result2 = pagingSource.prepend(key = result.prevKey) as LoadResult.Page
// prepend loads items in pos 46 - 50, should have 45 item before
assertThat(result2.itemsBefore).isEqualTo(45)
// append from initial load
- val result3 = pagingSource.append(key = result.nextKey)
+ val result3 = pagingSource.append(key = result.nextKey) as LoadResult.Page
// append loads items in position 66 - 70 , should have 65 item before
assertThat(result3.itemsBefore).isEqualTo(65)
@@ -478,19 +546,19 @@
dao.addAllItems(itemsList)
runBlocking {
// for initial load
- val result = pagingSource.refresh(key = 30)
+ val result = pagingSource.refresh(key = 30) as LoadResult.Page
// initial loads items in position 31 - 45, should have 55 items after
assertThat(result.itemsAfter).isEqualTo(55)
// prepend from initial load
- val result2 = pagingSource.prepend(key = result.prevKey)
+ val result2 = pagingSource.prepend(key = result.prevKey) as LoadResult.Page
// prepend loads items in position 26 - 30, should have 70 item after
assertThat(result2.itemsAfter).isEqualTo(70)
// append from initial load
- val result3 = pagingSource.append(result.nextKey)
+ val result3 = pagingSource.append(result.nextKey) as LoadResult.Page
// append loads items in position 46 - 50 , should have 50 item after
assertThat(result3.itemsAfter).isEqualTo(50)
@@ -503,7 +571,7 @@
dao.addAllItems(itemsList)
runBlocking {
// initial load
- val result = pagingSource.refresh()
+ val result = pagingSource.refresh() as LoadResult.Page
// 15 items loaded, assuming anchorPosition = 14 as the last item loaded
var refreshKey = pagingSource.getRefreshKey(
PagingState(
@@ -519,7 +587,7 @@
assertThat(refreshKey).isEqualTo(7)
// append after refresh
- val result2 = pagingSource.append(key = result.nextKey)
+ val result2 = pagingSource.append(key = result.nextKey) as LoadResult.Page
assertThat(result2.data).isEqualTo(
itemsList.subList(15, 20)
@@ -555,7 +623,7 @@
assertThat(refreshKey).isEqualTo(78)
val pagingSource2 = LimitOffsetPagingSourceImpl(database)
- val result2 = pagingSource2.refresh(key = refreshKey)
+ val result2 = pagingSource2.refresh(key = refreshKey) as LoadResult.Page
// database should only have 40 items left. Refresh key is invalid at this point
// (greater than item count after deletion)
@@ -604,7 +672,7 @@
// clips to 0
val refreshKey = 0
- val result2 = pagingSource2.refresh(key = refreshKey)
+ val result2 = pagingSource2.refresh(key = refreshKey) as LoadResult.Page
// database should only have 70 items left
assertThat(pagingSource2.itemCount.get()).isEqualTo(70)
@@ -640,7 +708,7 @@
// clips to 0
val refreshKey = 0
- val result2 = pagingSource2.refresh(key = refreshKey)
+ val result2 = pagingSource2.refresh(key = refreshKey) as LoadResult.Page
// database should only have 5 items left
assertThat(pagingSource2.itemCount.get()).isEqualTo(5)
@@ -705,35 +773,35 @@
private suspend fun PagingSource<Int, TestItem>.refresh(
key: Int? = null,
- ): PagingSource.LoadResult.Page<Int, TestItem> {
+ ): LoadResult<Int, TestItem> {
return this.load(
createLoadParam(
loadType = LoadType.REFRESH,
key = key,
)
- ) as PagingSource.LoadResult.Page
+ )
}
private suspend fun PagingSource<Int, TestItem>.append(
key: Int? = -1,
- ): PagingSource.LoadResult.Page<Int, TestItem> {
+ ): LoadResult<Int, TestItem> {
return this.load(
createLoadParam(
loadType = LoadType.APPEND,
key = key,
)
- ) as PagingSource.LoadResult.Page
+ )
}
private suspend fun PagingSource<Int, TestItem>.prepend(
key: Int? = -1,
- ): PagingSource.LoadResult.Page<Int, TestItem> {
+ ): LoadResult<Int, TestItem> {
return this.load(
createLoadParam(
loadType = LoadType.PREPEND,
key = key,
)
- ) as PagingSource.LoadResult.Page
+ )
}
companion object {
diff --git a/room/room-paging/src/main/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt b/room/room-paging/src/main/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
index 275a5a6..bceb2e7 100644
--- a/room/room-paging/src/main/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
+++ b/room/room-paging/src/main/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
@@ -40,6 +40,9 @@
* for Pager's consumption. Registers observers on tables lazily and automatically invalidates
* itself when data changes.
*/
+
+private val INVALID = PagingSource.LoadResult.Invalid<Any, Any>()
+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
abstract class LimitOffsetPagingSource<Value : Any>(
private val sourceQuery: RoomSQLiteQuery,
@@ -75,7 +78,12 @@
initialLoad(params)
} else {
// otherwise, it is a subsequent load
- loadFromDb(params, tempCount)
+ val loadResult = loadFromDb(params, tempCount)
+ // manually check if database has been updated. If so, the observers's
+ // invalidation callback will invalidate this paging source
+ db.invalidationTracker.refreshVersionsSync()
+ @Suppress("UNCHECKED_CAST")
+ if (invalid) INVALID as LoadResult.Invalid<Int, Value> else loadResult
}
}
}
diff --git a/settings.gradle b/settings.gradle
index cd01a47..87a385d 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])
@@ -284,6 +283,8 @@
includeProject(":compose:animation:animation-tooling-internal", "compose/animation/animation-tooling-internal", [BuildType.COMPOSE])
includeProject(":compose:animation:animation:integration-tests:animation-demos", "compose/animation/animation/integration-tests/animation-demos", [BuildType.COMPOSE])
includeProject(":compose:animation:animation:animation-samples", "compose/animation/animation/samples", [BuildType.COMPOSE])
+includeProject(":compose:animation:animation-graphics", "compose/animation/animation-graphics", [BuildType.COMPOSE])
+includeProject(":compose:animation:animation-graphics-samples", "compose/animation/animation-graphics/samples", [BuildType.COMPOSE])
includeProject(":compose:benchmark-utils", "compose/benchmark-utils", [BuildType.COMPOSE])
includeProject(":compose:benchmark-utils:benchmark-utils-benchmark", "compose/benchmark-utils/benchmark", [BuildType.COMPOSE])
includeProject(":compose:compiler:compiler", "compose/compiler/compiler", [BuildType.COMPOSE])
@@ -623,6 +624,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])
@@ -656,7 +659,7 @@
includeProject(":webkit:webkit", "webkit/webkit", [BuildType.MAIN])
includeProject(":window:window", "window/window", [BuildType.MAIN, BuildType.FLAN])
includeProject(":window:window-extensions", "window/window-extensions", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":window:window-java", "window/window-java", [BuildType.MAIN, BuildType.FLAN])
+includeProject(":window:window-java", "window/window-java", [BuildType.MAIN])
includeProject(":window:window-rxjava2", "window/window-rxjava2", [BuildType.MAIN])
includeProject(":window:window-rxjava3", "window/window-rxjava3", [BuildType.MAIN])
includeProject(":window:window-samples", "window/window-samples", [BuildType.MAIN])
diff --git a/slidingpanelayout/slidingpanelayout/build.gradle b/slidingpanelayout/slidingpanelayout/build.gradle
index 39d7795..a55b4ec 100644
--- a/slidingpanelayout/slidingpanelayout/build.gradle
+++ b/slidingpanelayout/slidingpanelayout/build.gradle
@@ -13,7 +13,6 @@
implementation("androidx.core:core:1.1.0")
api("androidx.customview:customview:1.1.0")
implementation(project(":window:window"))
- implementation(project(":window:window-java"))
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
diff --git a/slidingpanelayout/slidingpanelayout/src/androidTest/java/androidx/slidingpanelayout/widget/FoldingFeatureObserverTest.kt b/slidingpanelayout/slidingpanelayout/src/androidTest/java/androidx/slidingpanelayout/widget/FoldingFeatureObserverTest.kt
new file mode 100644
index 0000000..abe8864
--- /dev/null
+++ b/slidingpanelayout/slidingpanelayout/src/androidTest/java/androidx/slidingpanelayout/widget/FoldingFeatureObserverTest.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.slidingpanelayout.widget
+
+import androidx.slidingpanelayout.widget.helpers.TestActivity
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.window.FoldingFeature
+import androidx.window.WindowLayoutInfo
+import androidx.window.testing.FoldingFeature
+import androidx.window.testing.WindowLayoutInfoPublisherRule
+import androidx.window.windowInfoRepository
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+
+class FoldingFeatureObserverTest {
+ @get:Rule
+ val windowInfoPublisherRule: WindowLayoutInfoPublisherRule = WindowLayoutInfoPublisherRule()
+
+ @get:Rule
+ val activityScenarioRule: ActivityScenarioRule<TestActivity> =
+ ActivityScenarioRule(TestActivity::class.java)
+
+ @Test
+ fun testNoValuesBeforeSubscribe() {
+ val listener = TestListener()
+ activityScenarioRule.scenario.onActivity { activity ->
+ val observer = FoldingFeatureObserver(activity.windowInfoRepository(), Runnable::run)
+ val expected = FoldingFeature(activity = activity)
+ val info = WindowLayoutInfo.Builder().setDisplayFeatures(listOf(expected)).build()
+
+ observer.setOnFoldingFeatureChangeListener(listener)
+ windowInfoPublisherRule.overrideWindowLayoutInfo(info)
+
+ listener.assertCount(0)
+ }
+ }
+
+ @Test
+ fun testRelaysValuesFromWindowInfoRepo() {
+ val listener = TestListener()
+ activityScenarioRule.scenario.onActivity { activity ->
+ val observer = FoldingFeatureObserver(activity.windowInfoRepository(), Runnable::run)
+ val expected = FoldingFeature(activity = activity)
+ val info = WindowLayoutInfo.Builder().setDisplayFeatures(listOf(expected)).build()
+
+ observer.setOnFoldingFeatureChangeListener(listener)
+ observer.registerLayoutStateChangeCallback()
+ windowInfoPublisherRule.overrideWindowLayoutInfo(info)
+
+ listener.assertValue(expected)
+ }
+ }
+
+ @Test
+ fun testRelaysValuesNotRelayedAfterUnsubscribed() {
+ val listener = TestListener()
+ activityScenarioRule.scenario.onActivity { activity ->
+ val observer = FoldingFeatureObserver(activity.windowInfoRepository(), Runnable::run)
+ val expected = FoldingFeature(activity = activity)
+ val info = WindowLayoutInfo.Builder().setDisplayFeatures(listOf(expected)).build()
+
+ observer.setOnFoldingFeatureChangeListener(listener)
+ observer.registerLayoutStateChangeCallback()
+ observer.unregisterLayoutStateChangeCallback()
+ windowInfoPublisherRule.overrideWindowLayoutInfo(info)
+
+ listener.assertCount(0)
+ }
+ }
+
+ private class TestListener : FoldingFeatureObserver.OnFoldingFeatureChangeListener {
+ private val features = mutableListOf<FoldingFeature>()
+
+ override fun onFoldingFeatureChange(foldingFeature: FoldingFeature) {
+ features.add(foldingFeature)
+ }
+
+ fun assertCount(count: Int) {
+ assertEquals(count, features.size)
+ }
+
+ fun assertValue(expected: FoldingFeature) {
+ assertCount(1)
+ assertEquals(expected, features.first())
+ }
+ }
+}
\ No newline at end of file
diff --git a/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/FoldingFeatureObserver.kt b/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/FoldingFeatureObserver.kt
new file mode 100644
index 0000000..4d216a4
--- /dev/null
+++ b/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/FoldingFeatureObserver.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.slidingpanelayout.widget
+
+import android.app.Activity
+import androidx.window.FoldingFeature
+import androidx.window.WindowInfoRepo
+import androidx.window.WindowLayoutInfo
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.launch
+import java.util.concurrent.Executor
+
+/**
+ * A device folding feature observer is used to notify listener when there is a folding feature
+ * change.
+ */
+internal class FoldingFeatureObserver(
+ private val windowInfoRepo: WindowInfoRepo,
+ private val executor: Executor
+) {
+ private var job: Job? = null
+ private var onFoldingFeatureChangeListener: OnFoldingFeatureChangeListener? = null
+
+ /**
+ * Interface definition for a callback to be invoked when there is a folding feature change
+ */
+ internal interface OnFoldingFeatureChangeListener {
+ /**
+ * Callback method to update window layout when there is a folding feature change
+ */
+ fun onFoldingFeatureChange(foldingFeature: FoldingFeature)
+ }
+
+ /**
+ * Register a listener that can be notified when there is a folding feature change.
+ *
+ * @param onFoldingFeatureChangeListener The listener to be added
+ */
+ fun setOnFoldingFeatureChangeListener(
+ onFoldingFeatureChangeListener: OnFoldingFeatureChangeListener
+ ) {
+ this.onFoldingFeatureChangeListener = onFoldingFeatureChangeListener
+ }
+
+ /**
+ * Registers a callback for layout changes of the window for the supplied [Activity].
+ * Must be called only after the it is attached to the window.
+ */
+ fun registerLayoutStateChangeCallback() {
+ job?.cancel()
+ job = CoroutineScope(executor.asCoroutineDispatcher()).launch {
+ windowInfoRepo.windowLayoutInfo
+ .mapNotNull { info -> getFoldingFeature(info) }
+ .distinctUntilChanged()
+ .collect { nextFeature ->
+ onFoldingFeatureChangeListener?.onFoldingFeatureChange(nextFeature)
+ }
+ }
+ }
+
+ /**
+ * Unregisters a callback for window layout changes of the [Activity] window.
+ */
+ fun unregisterLayoutStateChangeCallback() {
+ job?.cancel()
+ }
+
+ private fun getFoldingFeature(windowLayoutInfo: WindowLayoutInfo): FoldingFeature? {
+ return windowLayoutInfo.displayFeatures
+ .firstOrNull { feature -> feature is FoldingFeature } as? FoldingFeature
+ }
+}
\ No newline at end of file
diff --git a/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java b/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
index 6b45a92..8d1e1c1d 100644
--- a/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
+++ b/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
@@ -45,7 +45,6 @@
import androidx.annotation.VisibleForTesting;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
-import androidx.core.util.Consumer;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
@@ -53,11 +52,8 @@
import androidx.customview.view.AbsSavedState;
import androidx.customview.widget.Openable;
import androidx.customview.widget.ViewDragHelper;
-import androidx.window.DisplayFeature;
import androidx.window.FoldingFeature;
import androidx.window.WindowInfoRepo;
-import androidx.window.WindowLayoutInfo;
-import androidx.window.java.WindowInfoRepoCallbackAdapter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -331,7 +327,11 @@
mDragHelper.setMinVelocity(MIN_FLING_VELOCITY * density);
try {
- FoldingFeatureObserver foldingFeatureObserver = new FoldingFeatureObserver(context);
+ Activity activity = requireActivity(context);
+ WindowInfoRepo repo = WindowInfoRepo.create(activity);
+ Executor mainExecutor = ContextCompat.getMainExecutor(activity);
+ FoldingFeatureObserver foldingFeatureObserver = new FoldingFeatureObserver(repo,
+ mainExecutor);
setFoldingFeatureObserver(foldingFeatureObserver);
} catch (IllegalArgumentException exception) {
// Disable fold detection.
@@ -1880,108 +1880,17 @@
return foldRectInView;
}
- /**
- * A device folding feature observer is used to notify listener when there is a folding feature
- * change.
- */
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- static class FoldingFeatureObserver {
- /**
- * Interface definition for a callback to be invoked when there is a folding feature change
- */
- private interface OnFoldingFeatureChangeListener {
- /**
- * Callback method to update window layout when there is a folding feature change
- */
- void onFoldingFeatureChange(@NonNull FoldingFeature foldingFeature);
- }
-
- class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
- private FoldingFeature mLastFoldingFeature;
-
- @Override
- public void accept(WindowLayoutInfo windowLayoutInfo) {
- final FoldingFeature currentFoldingFeature = getFoldingFeature(windowLayoutInfo);
- if (currentFoldingFeature != null) {
- // Update window layout when folding feature changed
- if (!currentFoldingFeature.equals(mLastFoldingFeature)) {
- dispatchOnFoldingFeatureChange(currentFoldingFeature);
- }
- mLastFoldingFeature = currentFoldingFeature;
- }
+ private static Activity requireActivity(Context context) {
+ Context iterator = context;
+ while (iterator instanceof ContextWrapper) {
+ if (iterator instanceof Activity) {
+ return (Activity) iterator;
}
-
- private FoldingFeature getFoldingFeature(WindowLayoutInfo windowLayoutInfo) {
- for (DisplayFeature displayFeature : windowLayoutInfo.getDisplayFeatures()) {
- if (displayFeature instanceof FoldingFeature) {
- return (FoldingFeature) displayFeature;
- }
- }
- return null;
- }
+ iterator = ((ContextWrapper) iterator).getBaseContext();
}
-
- private final WindowInfoRepoCallbackAdapter mWindowInfoRepo;
- private final Executor mExecutor;
- private OnFoldingFeatureChangeListener mOnFoldingFeatureChangeListener;
- private final LayoutStateChangeCallback mLayoutStateChangeCallback =
- new LayoutStateChangeCallback();
-
- /**
- * Create an instance of a folding feature observer
- *
- * @param context A visual context, such as an {@link Activity} or a {@link ContextWrapper}
- */
- FoldingFeatureObserver(@NonNull Context context) {
- Activity activity = requireActivity(context);
- mWindowInfoRepo = new WindowInfoRepoCallbackAdapter(WindowInfoRepo.create(activity));
- mExecutor = ContextCompat.getMainExecutor(context);
- }
-
- /**
- * Register a listener that can be notified when there is a folding feature change.
- *
- * @param onFoldingFeatureChangeListener The listener to be added
- */
- void setOnFoldingFeatureChangeListener(
- @NonNull OnFoldingFeatureChangeListener onFoldingFeatureChangeListener) {
- mOnFoldingFeatureChangeListener = onFoldingFeatureChangeListener;
- }
-
- void dispatchOnFoldingFeatureChange(@NonNull FoldingFeature foldingFeature) {
- if (mOnFoldingFeatureChangeListener == null) {
- return;
- }
- mOnFoldingFeatureChangeListener.onFoldingFeatureChange(foldingFeature);
- }
-
- /**
- * Registers a callback for layout changes of the window for the supplied {@link Activity}.
- * Must be called only after the it is attached to the window.
- */
- void registerLayoutStateChangeCallback() {
- mWindowInfoRepo.addWindowLayoutInfoListener(mExecutor, mLayoutStateChangeCallback);
- }
-
- /**
- * Unregisters a callback for window layout changes of the {@link Activity} window.
- */
- void unregisterLayoutStateChangeCallback() {
- mWindowInfoRepo.removeWindowLayoutInfoListener(mLayoutStateChangeCallback);
- }
-
- private static Activity requireActivity(Context context) {
- Context iterator = context;
- while (iterator instanceof ContextWrapper) {
- if (iterator instanceof Activity) {
- return (Activity) iterator;
- }
- iterator = ((ContextWrapper) iterator).getBaseContext();
- }
- throw new IllegalArgumentException("Used non-visual Context to obtain an instance of "
- + "WindowManager. Please use an Activity or a ContextWrapper around one "
- + "instead."
- );
- }
+ throw new IllegalArgumentException("Used non-visual Context to obtain an instance of "
+ + "WindowManager. Please use an Activity or a ContextWrapper around one "
+ + "instead."
+ );
}
}
diff --git a/startup/startup-runtime/src/main/baseline-prof.txt b/startup/startup-runtime/src/main/baseline-prof.txt
new file mode 100644
index 0000000..ea3d5d7
--- /dev/null
+++ b/startup/startup-runtime/src/main/baseline-prof.txt
@@ -0,0 +1,4 @@
+# Baseline Profile Rules for androidx.startup
+
+Landroidx/startup/AppInitializer;
+HSPLandroidx/startup/AppInitializer;->**(**)**
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/foundation/api/current.txt b/wear/compose/foundation/api/current.txt
index f7ec156..a9f1f91 100644
--- a/wear/compose/foundation/api/current.txt
+++ b/wear/compose/foundation/api/current.txt
@@ -15,7 +15,25 @@
}
public final class CurvedRowKt {
- method @androidx.compose.runtime.Composable public static void CurvedRow(optional androidx.compose.ui.Modifier modifier, optional float anchor, optional float anchorType, optional boolean clockwise, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void CurvedRow(optional androidx.compose.ui.Modifier modifier, optional float anchor, optional float anchorType, optional float radialAlignment, optional boolean clockwise, kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.CurvedRowScope,kotlin.Unit> content);
+ }
+
+ @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable public interface CurvedRowScope {
+ method public androidx.compose.ui.Modifier radialAlignment(androidx.compose.ui.Modifier, float alignment);
+ }
+
+ public final inline class RadialAlignment {
+ ctor public RadialAlignment();
+ }
+
+ public static final class RadialAlignment.Companion {
+ method public float Custom(float ratio);
+ method public float getCenter();
+ method public float getInner();
+ method public float getOuter();
+ property public final float Center;
+ property public final float Inner;
+ property public final float Outer;
}
}
diff --git a/wear/compose/foundation/api/public_plus_experimental_current.txt b/wear/compose/foundation/api/public_plus_experimental_current.txt
index f7ec156..a9f1f91 100644
--- a/wear/compose/foundation/api/public_plus_experimental_current.txt
+++ b/wear/compose/foundation/api/public_plus_experimental_current.txt
@@ -15,7 +15,25 @@
}
public final class CurvedRowKt {
- method @androidx.compose.runtime.Composable public static void CurvedRow(optional androidx.compose.ui.Modifier modifier, optional float anchor, optional float anchorType, optional boolean clockwise, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void CurvedRow(optional androidx.compose.ui.Modifier modifier, optional float anchor, optional float anchorType, optional float radialAlignment, optional boolean clockwise, kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.CurvedRowScope,kotlin.Unit> content);
+ }
+
+ @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable public interface CurvedRowScope {
+ method public androidx.compose.ui.Modifier radialAlignment(androidx.compose.ui.Modifier, float alignment);
+ }
+
+ public final inline class RadialAlignment {
+ ctor public RadialAlignment();
+ }
+
+ public static final class RadialAlignment.Companion {
+ method public float Custom(float ratio);
+ method public float getCenter();
+ method public float getInner();
+ method public float getOuter();
+ property public final float Center;
+ property public final float Inner;
+ property public final float Outer;
}
}
diff --git a/wear/compose/foundation/api/restricted_current.txt b/wear/compose/foundation/api/restricted_current.txt
index f7ec156..a9f1f91 100644
--- a/wear/compose/foundation/api/restricted_current.txt
+++ b/wear/compose/foundation/api/restricted_current.txt
@@ -15,7 +15,25 @@
}
public final class CurvedRowKt {
- method @androidx.compose.runtime.Composable public static void CurvedRow(optional androidx.compose.ui.Modifier modifier, optional float anchor, optional float anchorType, optional boolean clockwise, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void CurvedRow(optional androidx.compose.ui.Modifier modifier, optional float anchor, optional float anchorType, optional float radialAlignment, optional boolean clockwise, kotlin.jvm.functions.Function1<? super androidx.wear.compose.foundation.CurvedRowScope,kotlin.Unit> content);
+ }
+
+ @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable public interface CurvedRowScope {
+ method public androidx.compose.ui.Modifier radialAlignment(androidx.compose.ui.Modifier, float alignment);
+ }
+
+ public final inline class RadialAlignment {
+ ctor public RadialAlignment();
+ }
+
+ public static final class RadialAlignment.Companion {
+ method public float Custom(float ratio);
+ method public float getCenter();
+ method public float getInner();
+ method public float getOuter();
+ property public final float Center;
+ property public final float Inner;
+ property public final float Outer;
}
}
diff --git a/wear/compose/foundation/build.gradle b/wear/compose/foundation/build.gradle
index 21cdf26..b0d3670 100644
--- a/wear/compose/foundation/build.gradle
+++ b/wear/compose/foundation/build.gradle
@@ -31,13 +31,13 @@
kotlinPlugin("androidx.compose.compiler:compiler:1.0.0-rc01")
if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
- api(project(":compose:foundation:foundation"))
- api(project(":compose:ui:ui"))
- api(project(":compose:ui:ui-text"))
- api(project(":compose:runtime:runtime"))
+ api("androidx.compose.foundation:foundation:1.0.0-rc01")
+ api("androidx.compose.ui:ui:1.0.0-rc01")
+ api("androidx.compose.ui:ui-text:1.0.0-rc01")
+ api("androidx.compose.runtime:runtime:1.0.0-rc01")
implementation(libs.kotlinStdlib)
- implementation(project(":compose:foundation:foundation-layout"))
+ implementation("androidx.compose.foundation:foundation-layout:1.0.0-rc01")
androidTestImplementation project(path: ':compose:ui:ui-test')
androidTestImplementation project(path: ':compose:ui:ui-test-junit4')
@@ -62,7 +62,7 @@
api(project(":compose:ui:ui-text"))
api(project(":compose:runtime:runtime"))
- implementation(libs.kotlinStdlibCommon)
+ implementation(libs.kotlinStdlib)
implementation(project(":compose:foundation:foundation-layout"))
}
jvmMain.dependencies {
diff --git a/wear/compose/foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/CurvedRowSample.kt b/wear/compose/foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/CurvedRowSample.kt
index 0a1baba..17307a7 100644
--- a/wear/compose/foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/CurvedRowSample.kt
+++ b/wear/compose/foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/CurvedRowSample.kt
@@ -34,9 +34,7 @@
@Sampled
@Composable
fun SimpleCurvedRow() {
- CurvedRow(
- modifier = Modifier.fillMaxSize()
- ) {
+ CurvedRow(modifier = Modifier.fillMaxSize()) {
BasicText(
"Simple",
Modifier.background(Color.White).padding(2.dp),
diff --git a/wear/compose/foundation/src/androidAndroidTest/kotlin/AndroidManifest.xml b/wear/compose/foundation/src/androidAndroidTest/kotlin/AndroidManifest.xml
new file mode 100644
index 0000000..55fe1cd
--- /dev/null
+++ b/wear/compose/foundation/src/androidAndroidTest/kotlin/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.compose.foundation" />
diff --git a/wear/compose/foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/CurvedRowTest.kt b/wear/compose/foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/CurvedRowTest.kt
new file mode 100644
index 0000000..c0298d5
--- /dev/null
+++ b/wear/compose/foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/CurvedRowTest.kt
@@ -0,0 +1,305 @@
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.AnchorType
+import androidx.wear.compose.foundation.CurvedRow
+import androidx.wear.compose.foundation.RadialAlignment
+import org.junit.Assert.assertEquals
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.Test
+import kotlin.math.PI
+import kotlin.math.atan2
+import kotlin.math.min
+
+// When components are laid out, position is specified by integers, so we can't expect
+// much precision.
+internal const val FLOAT_TOLERANCE = 1f
+
+class CurvedRowTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ private fun anchor_and_clockwise_test(
+ anchor: Float,
+ anchorType: AnchorType,
+ clockwise: Boolean,
+ dimensionExtractor: (RadialDimensions) -> Float
+ ) {
+ var rowCoords: LayoutCoordinates? = null
+ var coords: LayoutCoordinates? = null
+ rule.setContent {
+ CurvedRow(
+ modifier = Modifier.size(200.dp)
+ .onGloballyPositioned { rowCoords = it },
+ anchor = anchor,
+ anchorType = anchorType,
+ clockwise = clockwise
+ ) {
+ Box(
+ modifier = Modifier
+ .size(40.dp)
+ .onGloballyPositioned { coords = it }
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ val dims = RadialDimensions(
+ clockwise = clockwise,
+ rowCoords!!,
+ coords!!
+ )
+
+ // It's at the outer side of the CurvedRow,
+ assertEquals(dims.rowRadius, dims.outerRadius, FLOAT_TOLERANCE)
+
+ checkAngle(anchor, dimensionExtractor(dims))
+ }
+ }
+
+ @Test
+ fun correctly_uses_anchortype_start_clockwise() =
+ anchor_and_clockwise_test(0f, AnchorType.Start, true) { it.startAngle }
+
+ @Test
+ fun correctly_uses_anchortype_center_clockwise() =
+ anchor_and_clockwise_test(60f, AnchorType.Center, true) { it.middleAngle }
+
+ @Test
+ fun correctly_uses_anchortype_end_clockwise() =
+ anchor_and_clockwise_test(120f, AnchorType.End, true) { it.endAngle }
+
+ @Test
+ fun correctly_uses_anchortype_start_anticlockwise() =
+ anchor_and_clockwise_test(180f, AnchorType.Start, false) { it.endAngle }
+
+ @Test
+ fun correctly_uses_anchortype_center_anticlockwise() =
+ anchor_and_clockwise_test(240f, AnchorType.Center, false) { it.middleAngle }
+
+ @Test
+ fun correctly_uses_anchortype_end_anticlockwise() =
+ anchor_and_clockwise_test(300f, AnchorType.End, false) { it.startAngle }
+
+ @Test
+ fun lays_out_multiple_children_correctly() {
+ var rowCoords: LayoutCoordinates? = null
+ val coords = Array<LayoutCoordinates?>(3) { null }
+ rule.setContent {
+ CurvedRow(
+ modifier = Modifier.onGloballyPositioned { rowCoords = it }
+ ) {
+ repeat(3) { ix ->
+ Box(
+ modifier = Modifier
+ .size(40.dp)
+ .onGloballyPositioned { coords[ix] = it }
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ val dims = coords.map {
+ RadialDimensions(
+ clockwise = true,
+ rowCoords!!,
+ it!!
+ )
+ }
+
+ dims.forEach {
+ // They are all at the outer side of the CurvedRow,
+ // and have the same innerRadius and sweep
+ assertEquals(it.rowRadius, it.outerRadius, FLOAT_TOLERANCE)
+ assertEquals(dims[0].innerRadius, it.innerRadius, FLOAT_TOLERANCE)
+ assertEquals(dims[0].sweep, it.sweep, FLOAT_TOLERANCE)
+ }
+ // There are one after another, the middle child is centered at 12 o clock
+ checkAngle(dims[0].endAngle, dims[1].startAngle)
+ checkAngle(dims[1].endAngle, dims[2].startAngle)
+ checkAngle(270f, dims[1].middleAngle)
+ }
+ }
+
+ private fun radial_alignment_test(
+ radialAlignment: RadialAlignment,
+ checker: (bigBoxDimensions: RadialDimensions, smallBoxDimensions: RadialDimensions) -> Unit
+ ) {
+ var rowCoords: LayoutCoordinates? = null
+ var smallBoxCoords: LayoutCoordinates? = null
+ var bigBoxCoords: LayoutCoordinates? = null
+ // We have a big box and a small box with the specified alignment
+ rule.setContent {
+ CurvedRow(
+ modifier = Modifier.onGloballyPositioned { rowCoords = it }
+ ) {
+ Box(
+ modifier = Modifier
+ .size(40.dp)
+ .onGloballyPositioned { smallBoxCoords = it }
+ .radialAlignment(radialAlignment)
+ )
+ Box(
+ modifier = Modifier
+ .size(60.dp)
+ .onGloballyPositioned { bigBoxCoords = it }
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ val bigBoxDimensions = RadialDimensions(
+ clockwise = true,
+ rowCoords!!,
+ bigBoxCoords!!
+ )
+
+ val smallBoxDimensions = RadialDimensions(
+ clockwise = true,
+ rowCoords!!,
+ smallBoxCoords!!
+ )
+
+ // There are one after another
+ checkAngle(smallBoxDimensions.endAngle, bigBoxDimensions.startAngle)
+
+ checker(bigBoxDimensions, smallBoxDimensions)
+ }
+ }
+
+ @Test
+ fun radial_alignment_outer_works() =
+ radial_alignment_test(RadialAlignment.Outer) { bigBoxDimensions, smallBoxDimensions ->
+ assertEquals(
+ bigBoxDimensions.outerRadius,
+ smallBoxDimensions.outerRadius,
+ FLOAT_TOLERANCE
+ )
+ }
+
+ @Test
+ fun radial_alignment_center_works() =
+ radial_alignment_test(RadialAlignment.Center) { bigBoxDimensions, smallBoxDimensions ->
+ assertEquals(
+ bigBoxDimensions.centerRadius,
+ smallBoxDimensions.centerRadius,
+ FLOAT_TOLERANCE
+ )
+ }
+
+ @Test
+ fun radial_alignment_inner_works() =
+ radial_alignment_test(RadialAlignment.Inner) { bigBoxDimensions, smallBoxDimensions ->
+ assertEquals(
+ bigBoxDimensions.innerRadius,
+ smallBoxDimensions.innerRadius,
+ FLOAT_TOLERANCE
+ )
+ }
+}
+
+fun checkAngle(expected: Float, actual: Float) {
+ var d = expected - actual
+ if (d < 0) d += 360f
+ if (d > 180) d = 360f - d
+ if (d > FLOAT_TOLERANCE) {
+ fail("Angle is out of tolerance. Expected: $expected, actual: $actual")
+ }
+}
+
+private fun Float.toRadians() = this * PI.toFloat() / 180f
+private fun Float.toDegrees() = this * 180f / PI.toFloat()
+
+private data class RadialPoint(val distance: Float, val angle: Float)
+
+// Utility class to compute the dimensions of the annulus segment corresponding to a given component
+// given that component's and the parent CurvedRow's LayoutCoordinates, and a boolean to indicate
+// if the layout is clockwise or counterclockwise
+private class RadialDimensions(
+ clockwise: Boolean,
+ rowCoords: LayoutCoordinates,
+ coords: LayoutCoordinates
+) {
+ // Row dimmensions
+ val rowCenter: Offset
+ val rowRadius: Float
+ // Component dimensions.
+ val innerRadius: Float
+ val outerRadius: Float
+ val centerRadius
+ get() = (innerRadius + outerRadius) / 2
+ val sweep: Float
+ val startAngle: Float
+ val middleAngle: Float
+ val endAngle: Float
+
+ init {
+ // Find the radius and center of the CurvedRow, all radial coordinates are relative to this
+ // center
+ rowRadius = min(rowCoords.size.width, rowCoords.size.height) / 2f
+ rowCenter = rowCoords.localToRoot(
+ Offset(
+ rowCoords.size.width / 2f,
+ rowCoords.size.height / 2f
+ )
+ )
+
+ // Compute the radial coordinates (relative to the center of the CurvedRow) of the found
+ // corners of the component's box and its center
+ val width = coords.size.width.toFloat()
+ val height = coords.size.height.toFloat()
+
+ val topLeft = toRadialCoordinates(coords, 0f, 0f)
+ val topRight = toRadialCoordinates(coords, width, 0f)
+ val center = toRadialCoordinates(coords, width / 2f, height / 2f)
+ val bottomLeft = toRadialCoordinates(coords, 0f, height)
+ val bottomRight = toRadialCoordinates(coords, width, height)
+
+ // Ensure the bottom corners are in the same circle
+ assertEquals(bottomLeft.distance, bottomRight.distance, FLOAT_TOLERANCE)
+ // Same with top corners
+ assertEquals(topLeft.distance, topRight.distance, FLOAT_TOLERANCE)
+
+ // Compute the four dimensions of the annulus sector
+ // Note that startAngle is always before endAngle (even when going counterclockwise)
+ if (clockwise) {
+ innerRadius = bottomLeft.distance
+ outerRadius = topLeft.distance
+ startAngle = bottomLeft.angle.toDegrees()
+ endAngle = bottomRight.angle.toDegrees()
+ } else {
+ // When components are laid out counterclockwise, they are rotated 180 degrees
+ innerRadius = topLeft.distance
+ outerRadius = bottomLeft.distance
+ startAngle = topRight.angle.toDegrees()
+ endAngle = topLeft.angle.toDegrees()
+ }
+
+ middleAngle = center.angle.toDegrees()
+ sweep = if (endAngle > startAngle) {
+ endAngle - startAngle
+ } else {
+ endAngle + 360f - startAngle
+ }
+
+ // All sweep angles are well between 0 and 90
+ assert((FLOAT_TOLERANCE..90f - FLOAT_TOLERANCE).contains(sweep)) { "sweep = $sweep" }
+
+ // The outerRadius is greater than the innerRadius
+ assert(outerRadius > innerRadius + FLOAT_TOLERANCE) {
+ "innerRadius = $innerRadius, outerRadius = $outerRadius"
+ }
+ }
+
+ private fun toRadialCoordinates(coords: LayoutCoordinates, x: Float, y: Float): RadialPoint {
+ val vector = coords.localToRoot(Offset(x, y)) - rowCenter
+ return RadialPoint(vector.getDistance(), atan2(vector.y, vector.x))
+ }
+}
\ No newline at end of file
diff --git a/wear/compose/foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedRow.kt b/wear/compose/foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedRow.kt
index a18bcc8..6a66a42 100644
--- a/wear/compose/foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedRow.kt
+++ b/wear/compose/foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedRow.kt
@@ -17,18 +17,27 @@
package androidx.wear.compose.foundation
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.LayoutScopeMarker
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.ParentDataModifier
import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
import kotlin.math.PI
import kotlin.math.asin
import kotlin.math.cos
import kotlin.math.min
+import kotlin.math.roundToInt
import kotlin.math.sin
import kotlin.math.sqrt
@@ -38,11 +47,19 @@
@Suppress("INLINE_CLASS_DEPRECATED")
inline class AnchorType internal constructor(internal val ratio: Float) {
companion object {
- // Start the content of the CurvedRow on the anchor
+ /**
+ * Start the content of the [CurvedRow] on the anchor
+ */
val Start = AnchorType(0f)
- // Center the content of the CurvedRow around the anchor
+
+ /**
+ * Center the content of the [CurvedRow] around the anchor
+ */
val Center = AnchorType(0.5f)
- // End the content of the CurvedRow on the anchor
+
+ /**
+ * End the content of the [CurvedRow] on the anchor
+ */
val End = AnchorType(1f)
}
@@ -56,6 +73,37 @@
}
/**
+ * How to lay down components when they are thinner than the [CurvedRow]. Similar to vertical
+ * alignment in a [Row].
+ */
+@Suppress("INLINE_CLASS_DEPRECATED")
+inline class RadialAlignment internal constructor(internal val ratio: Float) {
+ companion object {
+ /**
+ * Put the child closest to the center of the [CurvedRow], within the available space
+ */
+ val Inner = RadialAlignment(1f)
+
+ /**
+ * Put the child in the middle point of the available space.
+ */
+ val Center = RadialAlignment(0.5f)
+
+ /**
+ * Put the child farthest from the center of the [CurvedRow], within the available space
+ */
+ val Outer = RadialAlignment(0f)
+
+ /**
+ * Align the child in a custom position, 0 means Outer, 1 means Inner
+ */
+ fun Custom(ratio: Float): RadialAlignment {
+ return RadialAlignment(ratio)
+ }
+ }
+}
+
+/**
* A layout composable that places its children in an arc, rotating them as needed. This is
* similar to a [Row] layout, that it's curved into a segment of an annulus.
*
@@ -71,6 +119,10 @@
* Default is 270 degrees (top of the screen)
* @param anchorType Specify how the content is drawn with respect to the anchor. Default is to
* center the content on the anchor.
+ * @param radialAlignment Specifies the default radial alignment for children that don't specify
+ * one. Radial alignment specifies where to lay down children that are thiner than the
+ * CurvedRow, either closer to the center (INNER), apart from the center (OUTER) or in the middle
+ * point (CENTER).
* @param clockwise Specify if the children are laid out clockwise (the default) or
* counter-clockwise
*/
@@ -79,13 +131,17 @@
modifier: Modifier = Modifier,
anchor: Float = 270f,
anchorType: AnchorType = AnchorType.Center,
+ radialAlignment: RadialAlignment = RadialAlignment.Center,
clockwise: Boolean = true,
- content: @Composable () -> Unit
+ content: @Composable CurvedRowScope.() -> Unit
) {
// Note that all angles in the function are in radians, and the anchor parameter is in degrees
- Box(modifier = modifier) {
+ Box(
+ modifier = modifier,
+ contentAlignment = Alignment.Center
+ ) {
Layout(
- content = content
+ content = { CurvedRowScopeInstance.content() }
) { measurables, constraints ->
require(constraints.hasBoundedHeight || constraints.hasBoundedWidth)
// We take as much room as possible, the same in both dimensions, within the constraints
@@ -93,6 +149,7 @@
if (constraints.hasBoundedWidth) constraints.maxWidth else 0,
if (constraints.hasBoundedHeight) constraints.maxHeight else 0,
)
+ val radius = diameter / 2f
val measuredChildren = measurables.map { m ->
NormalMeasuredChild(m)
@@ -100,7 +157,15 @@
// Measure the children, we only need an upper bound for the thickness of each element.
measuredChildren.forEach {
- it.initialMeasurePass(diameter / 2f)
+ it.initialMeasurePass(radius)
+ }
+ val curvedRowThickness = measuredChildren.maxOfOrNull {
+ it.estimateThickness(radius)
+ } ?: 0f
+
+ // Now we can radially position the children
+ measuredChildren.forEach {
+ it.calculateRadialPosition(radius, curvedRowThickness, radialAlignment)
}
// Compute to total angle all children take and where we need to start laying them out.
@@ -115,7 +180,7 @@
val centerAngle = anchor.toRadians() + clockwiseFactor *
(childAngleStart + child.sweep / 2)
- child.place(diameter / 2f, scope = this, centerAngle, clockwise)
+ child.place(radius, scope = this, centerAngle, clockwise)
childAngleStart += child.sweep
}
@@ -124,16 +189,48 @@
}
}
+/**
+ * Layout scope used for modifiers (and children in the future) that only make sense in an CurvedRow
+ */
+@LayoutScopeMarker
+@Immutable
+interface CurvedRowScope {
+ /**
+ * Specify the radial positioning of this element inside the [CurvedRow]. Similar to vertical
+ * alignment in a [Row]
+ */
+ fun Modifier.radialAlignment(alignment: RadialAlignment): Modifier
+}
+
+internal object CurvedRowScopeInstance : CurvedRowScope {
+ override fun Modifier.radialAlignment(alignment: RadialAlignment): Modifier =
+ this.then(
+ RadialAlignmentImpl(
+ alignment,
+ inspectorInfo = debugInspectorInfo {
+ name = "radialAlignment"
+ properties["alignment"] = alignment
+ }
+ )
+ )
+}
private abstract class MeasuredChild(
val measurable: Measurable
) {
lateinit var placeable: Placeable
var width: Int = 0
var height: Int = 0
- var thickness: Float = 0f
var sweep: Float = 0f
+ var componentRadialPosition: Float = 0f
abstract fun initialMeasurePass(radius: Float)
+ abstract fun estimateThickness(radius: Float): Float
+ abstract fun calculateRadialPosition(
+ radius: Float,
+ curvedRowThickness: Float,
+ curvedRowRadialAlignment: RadialAlignment
+ )
+
abstract fun place(
radius: Float,
scope: Placeable.PlacementScope,
@@ -149,8 +246,8 @@
) {
with(scope) {
placeable.placeRelativeWithLayer(
- x = positionX.toInt(),
- y = positionY.toInt(),
+ x = positionX.roundToInt(),
+ y = positionY.roundToInt(),
layerBlock = {
rotationZ = rotation.toDegrees() - 270f
transformOrigin = TransformOrigin(0.5f, 0.5f)
@@ -164,20 +261,35 @@
override fun initialMeasurePass(radius: Float) {
// This is the size biggest square box that fits in half a circle
- val biggestSize = (radius * sqrt(2f)).toInt()
+ val biggestSize = (radius * 2 / sqrt(5f)).toInt()
val actualConstraint = Constraints(maxWidth = biggestSize, maxHeight = biggestSize)
placeable = measurable.measure(actualConstraint)
width = placeable.width
height = placeable.height
+ }
- // Distance we want from the center of the CurvedRow to the Top Center of the child's
- // containing box.
- val radiusInBox = sqrt(sqr(radius) - sqr(width / 2f))
+ override fun estimateThickness(radius: Float): Float {
+ // Compute the annulus we need as if the child was top aligned, this gives as an upper
+ // bound on the thickness, but we need to recompute later, when we know the actual position.
+ val (innerRadius, outerRadius) = computeAnnulusRadii(radius, 0f)
+ return outerRadius - innerRadius
+ }
- val innerRadius = sqrt(sqr(width / 2f) + sqr(radiusInBox - height))
- thickness = radius - innerRadius
+ override fun calculateRadialPosition(
+ radius: Float,
+ curvedRowThickness: Float,
+ curvedRowRadialAlignment: RadialAlignment
+ ) {
+ val radialAlignment = measurable.radialAlignment ?: curvedRowRadialAlignment
- sweep = 2 * asin(width / 2 / innerRadius)
+ // We know where we want it and the radial alignment, so we can compute it's positioning now
+ val (innerRadius, outerRadius) = computeAnnulusRadii(
+ radius - curvedRowThickness * radialAlignment.ratio,
+ radialAlignment.ratio
+ )
+ componentRadialPosition = radius - outerRadius
+
+ sweep = 2f * asin(width / 2f / innerRadius)
}
override fun place(
@@ -187,7 +299,7 @@
clockwise: Boolean
) {
// Distance from the center of the CurvedRow to the top left of the component.
- val radiusToTopLeft = radius
+ val radiusToTopLeft = radius - componentRadialPosition
// Distance from the center of the CurvedRow to the top center of the component.
val radiusToTopCenter = sqrt(sqr(radiusToTopLeft) - sqr(width / 2f))
@@ -195,7 +307,7 @@
// To position this child, we move its center rotating it around the CurvedRow's center.
val radiusToCenter = radiusToTopCenter - height / 2f
val childCenterX = radius + radiusToCenter * cos(centerAngle)
- val childCenterY = radius / 2f + radiusToCenter * sin(centerAngle)
+ val childCenterY = radius + radiusToCenter * sin(centerAngle)
// Then compute the position of the top left corner given that center.
val positionX = childCenterX - width / 2f
@@ -206,8 +318,62 @@
place(scope, positionX, positionY, rotationAngle)
}
+ /**
+ * Compute the inner and outer radii of the annulus sector required to fit the given box.
+ *
+ * @param targetRadius The distance we want, from the center of the circle the annulus is part
+ * of, to a point on the side of the box (which point is determined with the radiusAlpha
+ * parameter.)
+ * @param radiusAlpha Which point on the side of the box we are measuring the radius to. 0 means
+ * radius is to the outer point in the box, 1 means that it's to the inner point.
+ * (And interpolation in-between)
+ *
+ */
+ fun computeAnnulusRadii(targetRadius: Float, radiusAlpha: Float): Pair<Float, Float> {
+ // The top side of the triangles we use, squared.
+ val topSquared = sqr(width / 2f)
+
+ // Project the radius we know to the line going from the center to the circle to the center
+ // of the box
+ val radiusInBox = sqrt(sqr(targetRadius) - topSquared)
+
+ // Move to the top/bottom of the child box, then project back
+ val outerRadius = sqrt(topSquared + sqr(radiusInBox + radiusAlpha * height))
+ val innerRadius = sqrt(topSquared + sqr(radiusInBox - (1 - radiusAlpha) * height))
+
+ return innerRadius to outerRadius
+ }
+
fun sqr(x: Float): Float = x * x
}
private fun Float.toRadians() = this * PI.toFloat() / 180f
private fun Float.toDegrees() = this * 180f / PI.toFloat()
+
+internal class RadialAlignmentImpl(
+ private val radialAlignment: RadialAlignment,
+ inspectorInfo: InspectorInfo.() -> Unit
+) : ParentDataModifier, InspectorValueInfo(inspectorInfo) {
+ override fun Density.modifyParentData(parentData: Any?) =
+ ((parentData as? CurvedRowParentData) ?: CurvedRowParentData(radialAlignment))
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ return other is RadialAlignmentImpl && radialAlignment == other.radialAlignment
+ }
+
+ override fun hashCode(): Int = radialAlignment.hashCode()
+
+ override fun toString(): String =
+ "RadialAlignmentImpl($radialAlignment)"
+}
+
+/**
+ * Parent Data associated with children of a CurvedRow
+ */
+internal data class CurvedRowParentData(
+ var radialAlignment: RadialAlignment
+)
+
+internal val Measurable.radialAlignment: RadialAlignment?
+ get() = (parentData as? CurvedRowParentData)?.radialAlignment
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CardDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CardDemo.kt
new file mode 100644
index 0000000..a480cb27
--- /dev/null
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CardDemo.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.compose.integration.demos
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material.AppCard
+import androidx.wear.compose.material.Card
+import androidx.wear.compose.material.CardDefaults
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.Text
+import androidx.wear.compose.material.TitleCard
+
+@Composable
+fun CardDemo() {
+ Column(
+ modifier = Modifier.fillMaxSize()
+ .padding(start = 8.dp, end = 8.dp)
+ .verticalScroll(
+ rememberScrollState()
+ ),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Spacer(modifier = Modifier.size(30.dp))
+ Card(
+ onClick = {},
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Column(modifier = Modifier.fillMaxWidth()) {
+ Text("Basic unopinionated chip")
+ Text("Sets the shape")
+ Text("and the background")
+ }
+ }
+ Spacer(modifier = Modifier.size(4.dp))
+ AppCard(
+ onClick = {},
+ appName = { Text("AppName") },
+ title = { Text("AppCard") },
+ time = { Text("now") },
+ body = {
+ Column(modifier = Modifier.fillMaxWidth()) {
+ Text("Some body content")
+ Text("and some more body content")
+ }
+ },
+ )
+ Spacer(modifier = Modifier.size(4.dp))
+ TitleCard(
+ onClick = {},
+ title = { Text("TitleCard") },
+ time = { Text("now") },
+ body = {
+ Column(modifier = Modifier.fillMaxWidth()) {
+ Text("Some body content")
+ Text("and some more body content")
+ }
+ },
+ )
+ Spacer(modifier = Modifier.size(4.dp))
+ TitleCard(
+ onClick = {},
+ title = { Text("TitleCard") },
+ body = {
+ Column(modifier = Modifier.fillMaxWidth()) {
+ Text("This title card doesn't show time")
+ }
+ }
+ )
+ Spacer(modifier = Modifier.size(4.dp))
+ TitleCard(
+ onClick = {},
+ title = { Text("Custom TitleCard") },
+ body = {
+ Column(modifier = Modifier.fillMaxWidth()) {
+ Text("This title card emphasises the title with custom color")
+ }
+ },
+ titleColor = Color.Yellow
+ )
+ Spacer(modifier = Modifier.size(4.dp))
+ TitleCard(
+ onClick = {},
+ title = {
+ Column {
+ Text("Custom TitleCard")
+ Text("With a Coloured Secondary Label", color = Color.Yellow)
+ }
+ },
+ body = {
+ Column(modifier = Modifier.fillMaxWidth()) {
+ Text("This title card emphasises the title with custom color")
+ }
+ },
+ )
+ Spacer(modifier = Modifier.size(4.dp))
+ TitleCard(
+ onClick = {},
+ title = { Text("TitleCard With an ImageBackground") },
+ body = {
+ Column(modifier = Modifier.fillMaxWidth()) {
+ Text("Text coloured to stand out on the image")
+ }
+ },
+ backgroundPainter = CardDefaults.imageBackgroundPainter(
+ backgroundImagePainter = painterResource(id = R.drawable.backgroundimage1)
+ ),
+ bodyColor = MaterialTheme.colors.onSurface,
+ titleColor = MaterialTheme.colors.onSurface,
+ )
+ Spacer(modifier = Modifier.size(30.dp))
+ }
+}
\ No newline at end of file
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ChipDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ChipDemo.kt
new file mode 100644
index 0000000..4e21973
--- /dev/null
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ChipDemo.kt
@@ -0,0 +1,250 @@
+/*
+ * 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.compose.integration.demos
+
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material.Chip
+import androidx.wear.compose.material.ChipColors
+import androidx.wear.compose.material.ChipDefaults
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.Text
+import androidx.wear.compose.material.ToggleButton
+import androidx.wear.compose.material.ToggleChip
+import androidx.wear.compose.material.ToggleChipDefaults
+
+@Composable
+fun StandardChips() {
+ val scrollState: ScrollState = rememberScrollState()
+ var enabled by remember { mutableStateOf(true) }
+ var chipStyle by remember { mutableStateOf(ChipStyle.Primary) }
+
+ Column(
+ modifier = Modifier.verticalScroll(scrollState)
+ .padding(
+ PaddingValues(
+ start = 8.dp,
+ end = 8.dp,
+ top = 15.dp,
+ bottom = 50.dp
+ )
+ ),
+ verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically)
+ ) {
+
+ ChipCustomizerHeader(
+ enabled = enabled,
+ chipStyle = chipStyle,
+ onChipStyleChanged = { chipStyle = it },
+ onEnabledChanged = { enabled = it },
+ )
+ Text(
+ text = "Chip with Label",
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.caption1,
+ color = Color.White
+ )
+ DemoLabelChip(
+ label = "Single Label",
+ colors = chipColors(chipStyle),
+ enabled = enabled,
+ )
+ DemoLabelChip(
+ label = "Label that is long, to show truncation, we shouldn't be able to see more " +
+ "than 2 " +
+ "lines",
+ colors = chipColors(chipStyle),
+ enabled = enabled,
+ )
+ Text(
+ "Chip with icon",
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.caption1
+ )
+ DemoIconChip(
+ colors = chipColors(chipStyle),
+ label = "Label with icon",
+ enabled = enabled,
+ ) { DemoIcon(resourceId = R.drawable.ic_accessibility_24px) }
+
+ DemoIconChip(
+ colors = chipColors(chipStyle),
+ label = "Label that is long, to show truncation, we shouldn't be able to see more " +
+ "than 2 " +
+ "lines",
+ enabled = enabled,
+ ) { DemoIcon(resourceId = R.drawable.ic_accessibility_24px) }
+ Text(
+ "Primary + Secondary Label",
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.caption1
+ )
+ DemoLabelChip(
+ label = "Label",
+ secondaryLabel = "Secondary Label",
+ colors = chipColors(chipStyle),
+ enabled = enabled,
+ )
+ DemoLabelChip(
+ label = "Label that is long, to show truncation, we shouldn't be able to see more " +
+ "than 1 line",
+ secondaryLabel = "Secondary Label",
+ colors = chipColors(chipStyle),
+ enabled = enabled,
+ )
+ DemoIconChip(
+ colors = chipColors(chipStyle),
+ label = "Label with icon",
+ secondaryLabel = "Secondary Label",
+ enabled = enabled,
+ ) { DemoIcon(resourceId = R.drawable.ic_accessibility_24px) }
+ DemoIconChip(
+ colors = chipColors(chipStyle),
+ label = "Label that is long, to show truncation, we shouldn't be able to see more " +
+ "than 1 line",
+ secondaryLabel = "Long Secondary that is long, to show truncation, we shouldn't " +
+ "be able to see more than 1 line",
+ enabled = enabled,
+ ) { DemoIcon(resourceId = R.drawable.ic_accessibility_24px) }
+ }
+}
+
+@Composable
+private fun DemoIconChip(
+ colors: ChipColors,
+ label: String,
+ modifier: Modifier = Modifier,
+ secondaryLabel: String? = null,
+ enabled: Boolean = true,
+ content: @Composable (() -> Unit)? = null
+) {
+ val maxLabelLines = if (secondaryLabel != null) 1 else 2
+ Chip(
+ onClick = {},
+ modifier = modifier,
+ colors = colors,
+ label = {
+ Text(
+ text = label, maxLines = maxLabelLines,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ secondaryLabel = secondaryLabel?.let {
+ {
+ Text(
+ text = secondaryLabel,
+ maxLines = 1, overflow = TextOverflow.Ellipsis
+ )
+ }
+ },
+ icon = content, enabled = enabled
+ )
+}
+
+@Composable
+private fun DemoLabelChip(
+ colors: ChipColors,
+ label: String,
+ modifier: Modifier = Modifier,
+ secondaryLabel: String? = null,
+ enabled: Boolean = true
+) {
+ DemoIconChip(colors, label, modifier, secondaryLabel, enabled, null)
+}
+
+@Composable
+private fun ChipCustomizerHeader(
+ enabled: Boolean,
+ chipStyle: ChipStyle,
+ onEnabledChanged: ((enabled: Boolean) -> Unit),
+ onChipStyleChanged: ((chipStyle: ChipStyle) -> Unit),
+) {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically)
+ ) {
+ Text(
+ text = "Chip color",
+ style = MaterialTheme.typography.body2,
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ color = Color.White
+ )
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
+ modifier = Modifier.align(Alignment.CenterHorizontally).height(35.dp),
+ ) {
+ ChipStyle.values().forEach { style ->
+ ToggleButton(
+ checked = chipStyle == style,
+ onCheckedChange = {
+ onChipStyleChanged(style)
+ },
+ ) {
+ Text(
+ style = MaterialTheme.typography.caption2,
+ modifier = Modifier.padding(4.dp),
+ text = style.toString(),
+ )
+ }
+ }
+ }
+ ToggleChip(
+ checked = enabled,
+ onCheckedChange = onEnabledChanged,
+ label = {
+ Text("Chips enabled")
+ },
+ toggleIcon = {
+ ToggleChipDefaults.SwitchIcon(checked = enabled)
+ }
+ )
+ }
+}
+
+@Composable
+private fun chipColors(chipStyle: ChipStyle) =
+ when (chipStyle) {
+ ChipStyle.Primary -> ChipDefaults.primaryChipColors()
+ ChipStyle.Secondary -> ChipDefaults.secondaryChipColors()
+ ChipStyle.Child -> ChipDefaults.childChipColors()
+ }
+
+enum class ChipStyle {
+ Primary,
+ Secondary,
+ Child
+}
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CurvedRowDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CurvedRowDemo.kt
index d7d1e48..e64f4b5 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CurvedRowDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CurvedRowDemo.kt
@@ -31,75 +31,130 @@
import androidx.compose.ui.unit.sp
import androidx.wear.compose.foundation.AnchorType
import androidx.wear.compose.foundation.CurvedRow
+import androidx.wear.compose.foundation.CurvedRowScope
+import androidx.wear.compose.foundation.RadialAlignment
import androidx.wear.compose.material.Text
@Composable
fun CurvedRowDemo() {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ) {
- CurvedRow() {
- Box(
- modifier = Modifier
- .size(20.dp)
- .background(Color.Red)
+ CurvedRow(modifier = Modifier.fillMaxSize()) {
+ Box(
+ modifier = Modifier
+ .size(20.dp)
+ .background(Color.Red)
+ )
+ Column(
+ modifier = Modifier
+ .background(Color.Gray)
+ .padding(3.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = "A", color = Color.Black,
+ fontSize = 16.sp,
+ modifier = Modifier.background(Color.Blue)
)
- Column(
- modifier = Modifier
- .background(Color.Gray)
- .padding(3.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
+ Row {
Text(
- text = "A", color = Color.Black,
+ text = "B",
+ color = Color.Black,
fontSize = 16.sp,
- modifier = Modifier.background(Color.Blue)
+ modifier = Modifier.background(Color.Green).padding(2.dp)
)
- Row {
- Text(
- text = "B",
- color = Color.Black,
- fontSize = 16.sp,
- modifier = Modifier.background(Color.Green).padding(2.dp)
- )
- Text(
- text = "C",
- color = Color.Black,
- fontSize = 16.sp,
- modifier = Modifier.background(Color.Red)
- )
- }
+ Text(
+ text = "C",
+ color = Color.Black,
+ fontSize = 16.sp,
+ modifier = Modifier.background(Color.Red)
+ )
}
+ }
+ Box(
+ modifier = Modifier
+ .size(20.dp)
+ .background(Color.Red)
+ )
+ }
+ CurvedRow(
+ anchor = 90F,
+ anchorType = AnchorType.Start,
+ clockwise = false
+ ) {
+ Text(
+ text = "Start",
+ color = Color.Black,
+ fontSize = 30.sp,
+ modifier = Modifier.background(Color.White).padding(horizontal = 10.dp)
+ )
+ }
+ CurvedRow(
+ anchor = 90F,
+ anchorType = AnchorType.End,
+ clockwise = false
+ ) {
+ Text(
+ text = "End",
+ color = Color.Black,
+ fontSize = 30.sp,
+ modifier = Modifier.background(Color.White).padding(horizontal = 10.dp)
+ )
+ }
+}
+
+@Composable
+private fun CurvedRowScope.SeparatorBlock() {
+ Box(
+ modifier = Modifier
+ .size(10.dp, 40.dp)
+ .background(Color.Gray)
+ .radialAlignment(RadialAlignment.Outer)
+ )
+}
+
+@Composable
+private fun CurvedRowScope.RgbBlocks() {
+ Box(
+ modifier = Modifier
+ .size(20.dp)
+ .background(Color.Red)
+ .radialAlignment(RadialAlignment.Outer)
+ )
+ Box(
+ modifier = Modifier
+ .size(20.dp)
+ .background(Color.Green)
+ .radialAlignment(RadialAlignment.Center)
+ )
+ Box(
+ modifier = Modifier
+ .size(20.dp)
+ .background(Color.Blue)
+ .radialAlignment(RadialAlignment.Inner)
+ )
+}
+
+@Composable
+fun CurvedRowAlignmentDemo() {
+ CurvedRow(modifier = Modifier.fillMaxSize()) {
+ SeparatorBlock()
+ RgbBlocks()
+ SeparatorBlock()
+ (0..10).forEach {
Box(
modifier = Modifier
- .size(20.dp)
- .background(Color.Red)
+ .size(10.dp)
+ .background(Color.White)
+ .radialAlignment(RadialAlignment.Custom(it / 10.0f))
)
}
- CurvedRow(
- anchor = 90F,
- anchorType = AnchorType.Start,
- clockwise = false
- ) {
- Text(
- text = "Start",
- color = Color.Black,
- fontSize = 30.sp,
- modifier = Modifier.background(Color.White).padding(horizontal = 10.dp)
- )
- }
- CurvedRow(
- anchor = 90F,
- anchorType = AnchorType.End,
- clockwise = false
- ) {
- Text(
- text = "End",
- color = Color.Black,
- fontSize = 30.sp,
- modifier = Modifier.background(Color.White).padding(horizontal = 10.dp)
- )
- }
+ SeparatorBlock()
}
-}
\ No newline at end of file
+ CurvedRow(
+ anchor = 90f,
+ clockwise = false
+ ) {
+ SeparatorBlock()
+ RgbBlocks()
+ SeparatorBlock()
+ }
+}
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
index 7f5a536..f684a95 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
@@ -24,6 +24,7 @@
"Foundation",
listOf(
ComposableDemo("CurvedRow") { CurvedRowDemo() },
- ComposableDemo("Simple CurvedRow") { SimpleCurvedRow() },
+ ComposableDemo("Simple") { SimpleCurvedRow() },
+ ComposableDemo("Alignment") { CurvedRowAlignmentDemo() },
),
)
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
index 80814b4..7b75913 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
@@ -30,5 +30,12 @@
)
),
ComposableDemo("Toggle Button") { ToggleButtons() },
+ DemoCategory(
+ "Chips",
+ listOf(
+ ComposableDemo("Chip") { StandardChips() },
+ )
+ ),
+ ComposableDemo("Card") { CardDemo() },
),
)
diff --git a/wear/compose/integration-tests/demos/src/main/res/drawable/backgroundimage1.png b/wear/compose/integration-tests/demos/src/main/res/drawable/backgroundimage1.png
new file mode 100644
index 0000000..fbb9332
--- /dev/null
+++ b/wear/compose/integration-tests/demos/src/main/res/drawable/backgroundimage1.png
Binary files differ
diff --git a/wear/compose/integration-tests/macrobenchmark-target/build.gradle b/wear/compose/integration-tests/macrobenchmark-target/build.gradle
index 0182667..a506323 100644
--- a/wear/compose/integration-tests/macrobenchmark-target/build.gradle
+++ b/wear/compose/integration-tests/macrobenchmark-target/build.gradle
@@ -42,7 +42,7 @@
implementation(project(":compose:runtime:runtime"))
implementation(project(":compose:ui:ui-tooling"))
implementation(project(":activity:activity-compose"))
-// implementation() 'androidx.wear:wear:1.1.0'
+ implementation(project(":profileinstaller:profileinstaller"))
implementation project(path: ':wear:compose:compose-material')
}
diff --git a/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/StartupBenchmark.kt b/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/StartupBenchmark.kt
index 0b8754f..967cc01 100644
--- a/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/StartupBenchmark.kt
+++ b/wear/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/wear/compose/integration/macrobenchmark/StartupBenchmark.kt
@@ -48,6 +48,13 @@
companion object {
@Parameterized.Parameters(name = "startup={0},compilation={1}")
@JvmStatic
- fun parameters() = createStartupCompilationParams()
+ fun parameters() = createStartupCompilationParams(
+ compilationModes = listOf(
+ CompilationMode.None,
+ CompilationMode.Interpreted,
+ CompilationMode.SpeedProfile(),
+ CompilationMode.BaselineProfile,
+ )
+ )
}
}
\ No newline at end of file
diff --git a/wear/compose/material/api/current.txt b/wear/compose/material/api/current.txt
index b7a3b71..5f663c0 100644
--- a/wear/compose/material/api/current.txt
+++ b/wear/compose/material/api/current.txt
@@ -33,14 +33,16 @@
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.painter.Painter cardBackgroundPainter(optional long startBackgroundColor, optional long endBackgroundColor, optional androidx.compose.ui.unit.LayoutDirection gradientDirection);
method public float getAppImageSize();
method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.painter.Painter imageBackgroundPainter(androidx.compose.ui.graphics.painter.Painter backgroundImagePainter, optional androidx.compose.ui.graphics.Brush backgroundImageScrimBrush);
property public final float AppImageSize;
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
field public static final androidx.wear.compose.material.CardDefaults INSTANCE;
}
public final class CardKt {
- method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function0<kotlin.Unit> appName, kotlin.jvm.functions.Function0<kotlin.Unit> time, kotlin.jvm.functions.Function0<kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit> body, optional kotlin.jvm.functions.Function0<kotlin.Unit>? appImage, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long appColor, optional long timeColor, optional long titleColor, optional long bodyColor);
+ method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> appName, kotlin.jvm.functions.Function0<kotlin.Unit> time, kotlin.jvm.functions.Function0<kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit> body, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? appImage, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long appColor, optional long timeColor, optional long titleColor, optional long bodyColor);
method @androidx.compose.runtime.Composable public static void Card(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional boolean enabled, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void TitleCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit> body, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? time, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long titleColor, optional long timeColor, optional long bodyColor);
}
@androidx.compose.runtime.Stable public interface ChipColors {
diff --git a/wear/compose/material/api/public_plus_experimental_current.txt b/wear/compose/material/api/public_plus_experimental_current.txt
index b7a3b71..5f663c0 100644
--- a/wear/compose/material/api/public_plus_experimental_current.txt
+++ b/wear/compose/material/api/public_plus_experimental_current.txt
@@ -33,14 +33,16 @@
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.painter.Painter cardBackgroundPainter(optional long startBackgroundColor, optional long endBackgroundColor, optional androidx.compose.ui.unit.LayoutDirection gradientDirection);
method public float getAppImageSize();
method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.painter.Painter imageBackgroundPainter(androidx.compose.ui.graphics.painter.Painter backgroundImagePainter, optional androidx.compose.ui.graphics.Brush backgroundImageScrimBrush);
property public final float AppImageSize;
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
field public static final androidx.wear.compose.material.CardDefaults INSTANCE;
}
public final class CardKt {
- method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function0<kotlin.Unit> appName, kotlin.jvm.functions.Function0<kotlin.Unit> time, kotlin.jvm.functions.Function0<kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit> body, optional kotlin.jvm.functions.Function0<kotlin.Unit>? appImage, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long appColor, optional long timeColor, optional long titleColor, optional long bodyColor);
+ method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> appName, kotlin.jvm.functions.Function0<kotlin.Unit> time, kotlin.jvm.functions.Function0<kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit> body, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? appImage, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long appColor, optional long timeColor, optional long titleColor, optional long bodyColor);
method @androidx.compose.runtime.Composable public static void Card(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional boolean enabled, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void TitleCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit> body, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? time, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long titleColor, optional long timeColor, optional long bodyColor);
}
@androidx.compose.runtime.Stable public interface ChipColors {
diff --git a/wear/compose/material/api/restricted_current.txt b/wear/compose/material/api/restricted_current.txt
index b7a3b71..5f663c0 100644
--- a/wear/compose/material/api/restricted_current.txt
+++ b/wear/compose/material/api/restricted_current.txt
@@ -33,14 +33,16 @@
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.painter.Painter cardBackgroundPainter(optional long startBackgroundColor, optional long endBackgroundColor, optional androidx.compose.ui.unit.LayoutDirection gradientDirection);
method public float getAppImageSize();
method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.painter.Painter imageBackgroundPainter(androidx.compose.ui.graphics.painter.Painter backgroundImagePainter, optional androidx.compose.ui.graphics.Brush backgroundImageScrimBrush);
property public final float AppImageSize;
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
field public static final androidx.wear.compose.material.CardDefaults INSTANCE;
}
public final class CardKt {
- method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function0<kotlin.Unit> appName, kotlin.jvm.functions.Function0<kotlin.Unit> time, kotlin.jvm.functions.Function0<kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit> body, optional kotlin.jvm.functions.Function0<kotlin.Unit>? appImage, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long appColor, optional long timeColor, optional long titleColor, optional long bodyColor);
+ method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> appName, kotlin.jvm.functions.Function0<kotlin.Unit> time, kotlin.jvm.functions.Function0<kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit> body, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? appImage, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long appColor, optional long timeColor, optional long titleColor, optional long bodyColor);
method @androidx.compose.runtime.Composable public static void Card(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional boolean enabled, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void TitleCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> title, kotlin.jvm.functions.Function0<kotlin.Unit> body, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? time, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long titleColor, optional long timeColor, optional long bodyColor);
}
@androidx.compose.runtime.Stable public interface ChipColors {
diff --git a/wear/compose/material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/CardTest.kt b/wear/compose/material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/CardTest.kt
index ef3a6ee1..674c6da 100644
--- a/wear/compose/material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/CardTest.kt
+++ b/wear/compose/material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/CardTest.kt
@@ -226,14 +226,82 @@
) { MaterialTheme.colors.onSurfaceVariant2 }
@Test
- public fun app_chip_gives_default_colors(): Unit =
- verifyAppCardColors(
- { MaterialTheme.colors.primary },
- { MaterialTheme.colors.primary },
- { MaterialTheme.colors.onSurfaceVariant },
- { MaterialTheme.colors.onSurface },
- { MaterialTheme.colors.onSurfaceVariant2 },
- )
+ public fun app_card_gives_default_colors() {
+ var expectedAppImageColor = Color.Transparent
+ var expectedAppColor = Color.Transparent
+ var expectedTimeColor = Color.Transparent
+ var expectedTitleColor = Color.Transparent
+ var expectedBodyColor = Color.Transparent
+ var actualBodyColor = Color.Transparent
+ var actualTitleColor = Color.Transparent
+ var actualTimeColor = Color.Transparent
+ var actualAppColor = Color.Transparent
+ var actualAppImageColor = Color.Transparent
+ val testBackground = Color.White
+
+ rule.setContentWithTheme {
+ expectedAppImageColor = MaterialTheme.colors.primary
+ expectedAppColor = MaterialTheme.colors.primary
+ expectedTimeColor = MaterialTheme.colors.onSurfaceVariant
+ expectedTitleColor = MaterialTheme.colors.onSurface
+ expectedBodyColor = MaterialTheme.colors.onSurfaceVariant2
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(testBackground)
+ ) {
+ AppCard(
+ onClick = {},
+ appName = { actualAppColor = LocalContentColor.current },
+ appImage = { actualAppImageColor = LocalContentColor.current },
+ time = { actualTimeColor = LocalContentColor.current },
+ body = { actualBodyColor = LocalContentColor.current },
+ title = { actualTitleColor = LocalContentColor.current },
+ modifier = Modifier.testTag(TEST_TAG)
+ )
+ }
+ }
+
+ assertEquals(expectedAppImageColor, actualAppImageColor)
+ assertEquals(expectedAppColor, actualAppColor)
+ assertEquals(expectedTimeColor, actualTimeColor)
+ assertEquals(expectedTitleColor, actualTitleColor)
+ assertEquals(expectedBodyColor, actualBodyColor)
+ }
+
+ @Test
+ public fun title_card_gives_default_colors() {
+ var expectedTimeColor = Color.Transparent
+ var expectedTitleColor = Color.Transparent
+ var expectedBodyColor = Color.Transparent
+ var actualBodyColor = Color.Transparent
+ var actualTitleColor = Color.Transparent
+ var actualTimeColor = Color.Transparent
+ val testBackground = Color.White
+
+ rule.setContentWithTheme {
+ expectedTimeColor = MaterialTheme.colors.onSurfaceVariant
+ expectedTitleColor = MaterialTheme.colors.onSurface
+ expectedBodyColor = MaterialTheme.colors.onSurfaceVariant2
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(testBackground)
+ ) {
+ TitleCard(
+ onClick = {},
+ time = { actualTimeColor = LocalContentColor.current },
+ body = { actualBodyColor = LocalContentColor.current },
+ title = { actualTitleColor = LocalContentColor.current },
+ modifier = Modifier.testTag(TEST_TAG)
+ )
+ }
+ }
+
+ assertEquals(expectedTimeColor, actualTimeColor)
+ assertEquals(expectedTitleColor, actualTitleColor)
+ assertEquals(expectedBodyColor, actualBodyColor)
+ }
private fun verifyColors(
status: CardStatus,
@@ -261,55 +329,6 @@
assertEquals(expectedContent, actualContent)
}
-
- private fun verifyAppCardColors(
- appImageColor: @Composable () -> Color,
- appColor: @Composable () -> Color,
- timeColor: @Composable () -> Color,
- titleColor: @Composable () -> Color,
- bodyColor: @Composable () -> Color,
- ) {
- var expectedAppImageColor = Color.Transparent
- var expectedAppColor = Color.Transparent
- var expectedTimeColor = Color.Transparent
- var expectedTitleColor = Color.Transparent
- var expectedBodyColor = Color.Transparent
- var actualBodyColor = Color.Transparent
- var actualTitleColor = Color.Transparent
- var actualTimeColor = Color.Transparent
- var actualAppColor = Color.Transparent
- var actualAppImageColor = Color.Transparent
- val testBackground = Color.White
-
- rule.setContentWithTheme {
- expectedAppImageColor = appImageColor()
- expectedAppColor = appColor()
- expectedTimeColor = timeColor()
- expectedTitleColor = titleColor()
- expectedBodyColor = bodyColor()
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(testBackground)
- ) {
- AppCard(
- onClick = {},
- appName = { actualAppColor = LocalContentColor.current },
- appImage = { actualAppImageColor = LocalContentColor.current },
- time = { actualTimeColor = LocalContentColor.current },
- body = { actualBodyColor = LocalContentColor.current },
- title = { actualTitleColor = LocalContentColor.current },
- modifier = Modifier.testTag(TEST_TAG)
- )
- }
- }
-
- assertEquals(expectedAppImageColor, actualAppImageColor)
- assertEquals(expectedAppColor, actualAppColor)
- assertEquals(expectedTimeColor, actualTimeColor)
- assertEquals(expectedTitleColor, actualTitleColor)
- assertEquals(expectedBodyColor, actualBodyColor)
- }
}
public class CardFontTest {
@@ -373,6 +392,39 @@
assertEquals(expectedTitleTextStyle, actualTitleTextStyle)
assertEquals(expectedBodyTextStyle, actualBodyTextStyle)
}
+
+ @Test
+ public fun title_card_gives_correct_text_style_base() {
+ var actualTimeTextStyle = TextStyle.Default
+ var actualTitleTextStyle = TextStyle.Default
+ var actualBodyTextStyle = TextStyle.Default
+ var expectedTimeTextStyle = TextStyle.Default
+ var expectedTitleTextStyle = TextStyle.Default
+ var expectedBodyTextStyle = TextStyle.Default
+
+ rule.setContentWithTheme {
+ expectedTimeTextStyle = MaterialTheme.typography.caption1
+ expectedTitleTextStyle = MaterialTheme.typography.button
+ expectedBodyTextStyle = MaterialTheme.typography.body1
+
+ TitleCard(
+ onClick = {},
+ time = {
+ actualTimeTextStyle = LocalTextStyle.current
+ },
+ title = {
+ actualTitleTextStyle = LocalTextStyle.current
+ },
+ body = {
+ actualBodyTextStyle = LocalTextStyle.current
+ },
+ modifier = Modifier.testTag(TEST_TAG)
+ )
+ }
+ assertEquals(expectedTimeTextStyle, actualTimeTextStyle)
+ assertEquals(expectedTitleTextStyle, actualTitleTextStyle)
+ assertEquals(expectedBodyTextStyle, actualBodyTextStyle)
+ }
}
private fun ComposeContentTestRule.verifyHeight(expected: Dp, content: @Composable () -> Unit) {
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 566c192..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
@@ -40,6 +40,7 @@
import androidx.compose.ui.draw.paint
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.LinearGradientShader
import androidx.compose.ui.graphics.Shader
@@ -48,6 +49,7 @@
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.Dp
@@ -110,6 +112,7 @@
.matchParentSize()
.paint(
painter = backgroundPainter,
+ contentScale = ContentScale.Crop
)
val contentBoxModifier = Modifier
@@ -140,7 +143,10 @@
/**
* Opinionated Wear Material [Card] that offers a specific 5 slot layout to show information about
- * an application, e.g. a notification.
+ * an application, e.g. a notification. AppCards are designed to show interactive elements from
+ * multiple applications. They will typically be used by the system UI, e.g. for showing a list of
+ * notifications from different applications. However it could also be adapted by individual
+ * application developers to show information about different parts of their application.
*
* The first row of the layout has three slots, 1) a small optional application [Image] or [Icon] of
* size [CardDefaults.AppImageSize]x[CardDefaults.AppImageSize] dp, 2) an application name
@@ -154,13 +160,13 @@
* The rest of the [Card] contains the body content which can be either [Text] or an [Image].
*
* @param onClick Will be called when the user clicks the card
- * @param modifier Modifier to be applied to the card
* @param appName A slot for displaying the application name, expected to be a single line of text
* of [Typography.title3]
* @param time A slot for displaying the time relevant to the contents of the card, expected to be a
- * short piece of right aligned text.
+ * short piece of end aligned text.
* @param body A slot for displaying the details of the [Card], expected to be either [Text]
* (single or multiple-line) or an [Image]
+ * @param modifier Modifier to be applied to the card
* @param appImage A slot for a small ([CardDefaults.AppImageSize]x[CardDefaults.AppImageSize] )
* [Image] or [Icon] associated with the application.
* @param backgroundPainter A painter used to paint the background of the card. A card will
@@ -175,11 +181,11 @@
@Composable
public fun AppCard(
onClick: () -> Unit,
- modifier: Modifier = Modifier,
appName: @Composable () -> Unit,
time: @Composable () -> Unit,
title: @Composable () -> Unit,
body: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
appImage: @Composable (() -> Unit)? = null,
backgroundPainter: Painter = CardDefaults.cardBackgroundPainter(),
appColor: Color = MaterialTheme.colors.primary,
@@ -233,6 +239,88 @@
}
/**
+ * Opinionated Wear Material [Card] that offers a specific 3 slot layout to show interactive
+ * information about an application, e.g. a message. TitleCards are designed for use within an
+ * application.
+ *
+ * The first row of the layout has two slots. 1. a start aligned title (emphasised with the
+ * [titleColor] and expected to be start aligned text). The title text is expected to be a maximum
+ * of 2 lines of text. 2. An optional time that the application activity has occurred shown at the
+ * end of the row, expected to be an end aligned [Text] composable showing a time relevant to the
+ * contents of the [Card].
+ *
+ * The rest of the [Card] contains the body content which is expected to be [Text] or a contained
+ * [Image].
+ *
+ * Overall the [title] and [body] text should be no more than 5 rows of text combined.
+ *
+ * @param onClick Will be called when the user clicks the card
+ * @param title A slot for displaying the title of the card, expected to be one or two lines of text
+ * of [Typography.button]
+ * @param body A slot for displaying the details of the [Card], expected to be either [Text]
+ * (single or multiple-line) or an [Image]. If [Text] then it is expected to be a maximum of 4 lines
+ * of text of [Typography.body1]
+ * @param modifier Modifier to be applied to the card
+ * @param time An optional slot for displaying the time relevant to the contents of the card,
+ * expected to be a short piece of end aligned text.
+ * @param backgroundPainter A painter used to paint the background of the card. A title card can
+ * have either a gradient background or an image background, use
+ * [CardDefaults.cardBackgroundPainter()] or [CardDefaults.imageBackgroundPainter()] to obtain an
+ * appropriate painter
+ * @param titleColor The default color to use for title() slot unless explicitly set.
+ * @param timeColor The default color to use for time() slot unless explicitly set.
+ * @param bodyColor The default color to use for body() slot unless explicitly set.
+ */
+@Composable
+public fun TitleCard(
+ onClick: () -> Unit,
+ title: @Composable () -> Unit,
+ body: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ time: @Composable (() -> Unit)? = null,
+ backgroundPainter: Painter = CardDefaults.cardBackgroundPainter(),
+ titleColor: Color = MaterialTheme.colors.onSurface,
+ timeColor: Color = MaterialTheme.colors.onSurfaceVariant,
+ bodyColor: Color = MaterialTheme.colors.onSurfaceVariant2,
+) {
+ Card(
+ onClick = onClick,
+ modifier = modifier,
+ backgroundPainter = backgroundPainter,
+ enabled = true,
+ ) {
+ Column {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ CompositionLocalProvider(
+ LocalContentColor provides titleColor,
+ LocalTextStyle provides MaterialTheme.typography.button,
+ content = title
+ )
+ if (time != null) {
+ Spacer(modifier = Modifier.width(4.dp))
+ Box(modifier = Modifier.weight(1.0f), contentAlignment = Alignment.CenterEnd) {
+ CompositionLocalProvider(
+ LocalContentColor provides timeColor,
+ LocalTextStyle provides MaterialTheme.typography.caption1,
+ content = time
+ )
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(8.dp))
+ CompositionLocalProvider(
+ LocalContentColor provides bodyColor,
+ LocalTextStyle provides MaterialTheme.typography.body1,
+ content = body
+ )
+ }
+ }
+}
+
+/**
* Contains the default values used by [Card]
*/
public object CardDefaults {
@@ -274,6 +362,35 @@
return BrushPainter(FortyFiveDegreeLinearGradient(backgroundColors))
}
+ /**
+ * Creates a [Painter] for the background of a [Card] that displays an Image with a scrim over
+ * the image to make sure that any content above the background will be legible.
+ *
+ * An Image background is a means to reinforce the meaning of information in a Card, e.g. To
+ * help to contextualize the information in a TitleCard
+ *
+ * Cards should have a content color that contrasts with the background image and scrim
+ *
+ * @param backgroundImagePainter The [Painter] to use to draw the background of the [Card]
+ * @param backgroundImageScrimBrush The [Brush] to use to paint a scrim over the background
+ * image to ensure that any text drawn over the image is legible
+ */
+ @Composable
+ public fun imageBackgroundPainter(
+ backgroundImagePainter: Painter,
+ backgroundImageScrimBrush: Brush = Brush.linearGradient(
+ colors = listOf(
+ MaterialTheme.colors.surface.copy(alpha = 1.0f),
+ MaterialTheme.colors.surface.copy(alpha = 0f)
+ )
+ )
+ ): Painter {
+ return ImageWithScrimPainter(
+ imagePainter = backgroundImagePainter,
+ brush = backgroundImageScrimBrush
+ )
+ }
+
private val CardHorizontalPadding = 12.dp
private val CardVerticalPadding = 12.dp
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-input/src/main/java/androidx/wear/input/RemoteInputIntentHelper.kt b/wear/wear-input/src/main/java/androidx/wear/input/RemoteInputIntentHelper.kt
index bef71a8..6f19122 100644
--- a/wear/wear-input/src/main/java/androidx/wear/input/RemoteInputIntentHelper.kt
+++ b/wear/wear-input/src/main/java/androidx/wear/input/RemoteInputIntentHelper.kt
@@ -36,10 +36,20 @@
* );
* val intent: Intent = createActionRemoteInputIntent();
* putRemoteInputsExtra(intent, remoteInputs)
- * startActivity(intent);
+ * startActivityForResult(intent);
* ```
+ *
* The intent returned via [android.app.Activity.onActivityResult] will contain the input results if
- * collected. More information about accessing these results can be found in [RemoteInput].
+ * collected, for example:
+ *
+ * ```
+ * override fun onActivityResult(requestCode: Int, resultCode: Int, intentResults: Intent?) {
+ * val results: Bundle = RemoteInput.getResultsFromIntent(intentResults)
+ * val quickReplyResult: CharSequence? = results.getCharSequence(KEY_QUICK_REPLY_TEXT)
+ * }
+ * ```
+ *
+ * More information about accessing these results can be found in [RemoteInput].
*/
@RequiresApi(Build.VERSION_CODES.N)
public class RemoteInputIntentHelper private constructor() {
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..b057b3c 100644
--- a/wear/wear-phone-interactions/api/current.txt
+++ b/wear/wear-phone-interactions/api/current.txt
@@ -21,17 +21,21 @@
@RequiresApi(android.os.Build.VERSION_CODES.O) public final class CodeChallenge {
ctor public CodeChallenge(androidx.wear.phone.interactions.authentication.CodeVerifier codeVerifier);
method public String getValue();
+ property public final String value;
}
@RequiresApi(android.os.Build.VERSION_CODES.O) public final class CodeVerifier {
ctor public CodeVerifier(optional int byteLength);
ctor public CodeVerifier(String value);
method public String getValue();
+ property public final String value;
}
public final class OAuthRequest {
method public String getPackageName();
method public android.net.Uri getRequestUrl();
+ property public final String packageName;
+ property public final android.net.Uri requestUrl;
field public static final androidx.wear.phone.interactions.authentication.OAuthRequest.Companion Companion;
field public static final String WEAR_REDIRECT_URL_PREFIX = "https://wear.googleapis.com/3p_auth/";
}
@@ -51,6 +55,8 @@
public final class OAuthResponse {
method public int getErrorCode();
method public android.net.Uri? getResponseUrl();
+ property public final int errorCode;
+ property public final android.net.Uri? responseUrl;
}
public static final class OAuthResponse.Builder {
@@ -64,7 +70,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 +79,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 +136,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..b057b3c 100644
--- a/wear/wear-phone-interactions/api/public_plus_experimental_current.txt
+++ b/wear/wear-phone-interactions/api/public_plus_experimental_current.txt
@@ -21,17 +21,21 @@
@RequiresApi(android.os.Build.VERSION_CODES.O) public final class CodeChallenge {
ctor public CodeChallenge(androidx.wear.phone.interactions.authentication.CodeVerifier codeVerifier);
method public String getValue();
+ property public final String value;
}
@RequiresApi(android.os.Build.VERSION_CODES.O) public final class CodeVerifier {
ctor public CodeVerifier(optional int byteLength);
ctor public CodeVerifier(String value);
method public String getValue();
+ property public final String value;
}
public final class OAuthRequest {
method public String getPackageName();
method public android.net.Uri getRequestUrl();
+ property public final String packageName;
+ property public final android.net.Uri requestUrl;
field public static final androidx.wear.phone.interactions.authentication.OAuthRequest.Companion Companion;
field public static final String WEAR_REDIRECT_URL_PREFIX = "https://wear.googleapis.com/3p_auth/";
}
@@ -51,6 +55,8 @@
public final class OAuthResponse {
method public int getErrorCode();
method public android.net.Uri? getResponseUrl();
+ property public final int errorCode;
+ property public final android.net.Uri? responseUrl;
}
public static final class OAuthResponse.Builder {
@@ -64,7 +70,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 +79,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 +136,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..b057b3c 100644
--- a/wear/wear-phone-interactions/api/restricted_current.txt
+++ b/wear/wear-phone-interactions/api/restricted_current.txt
@@ -21,17 +21,21 @@
@RequiresApi(android.os.Build.VERSION_CODES.O) public final class CodeChallenge {
ctor public CodeChallenge(androidx.wear.phone.interactions.authentication.CodeVerifier codeVerifier);
method public String getValue();
+ property public final String value;
}
@RequiresApi(android.os.Build.VERSION_CODES.O) public final class CodeVerifier {
ctor public CodeVerifier(optional int byteLength);
ctor public CodeVerifier(String value);
method public String getValue();
+ property public final String value;
}
public final class OAuthRequest {
method public String getPackageName();
method public android.net.Uri getRequestUrl();
+ property public final String packageName;
+ property public final android.net.Uri requestUrl;
field public static final androidx.wear.phone.interactions.authentication.OAuthRequest.Companion Companion;
field public static final String WEAR_REDIRECT_URL_PREFIX = "https://wear.googleapis.com/3p_auth/";
}
@@ -51,6 +55,8 @@
public final class OAuthResponse {
method public int getErrorCode();
method public android.net.Uri? getResponseUrl();
+ property public final int errorCode;
+ property public final android.net.Uri? responseUrl;
}
public static final class OAuthResponse.Builder {
@@ -64,7 +70,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 +79,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 +136,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/CodeChallenge.kt b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/CodeChallenge.kt
index 3b10dcf..b1a4123 100644
--- a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/CodeChallenge.kt
+++ b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/CodeChallenge.kt
@@ -22,22 +22,22 @@
import java.security.MessageDigest
import java.util.Base64
+/* ktlint-disable max-line-length */
/**
- * Authorization code challenge
+ * Authorization code challenge.
*
- * * Related specifications:
- * Proof Key for Code Exchange by OAuth Public Clients (RFC 7636).
- * https://tools.ietf.org/html/rfc7636
+ * Related specifications:
+ * [Proof Key for Code Exchange by OAuth Public Clients (RFC 7636)](https://tools.ietf.org/html/rfc7636)
*/
+/* ktlint-enable max-line-length */
@RequiresApi(Build.VERSION_CODES.O)
public class CodeChallenge constructor(
codeVerifier: CodeVerifier
) {
-
/**
* The challenge value.
*/
- private var value: String? = null
+ public val value: String
/**
* Computes the code challenge value using the specified verifier with SHA-256.
@@ -48,18 +48,14 @@
value = Base64.getUrlEncoder().withoutPadding().encodeToString(hash)
}
- public fun getValue(): String {
- return value!!
- }
-
override fun equals(other: Any?): Boolean {
if (other is CodeChallenge) {
- return other.getValue() == value
+ return other.value == value
}
return false
}
override fun hashCode(): Int {
- return value!!.toByteArray(StandardCharsets.UTF_8).contentHashCode()
+ return value.toByteArray(StandardCharsets.UTF_8).contentHashCode()
}
}
\ No newline at end of file
diff --git a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/CodeVerifier.kt b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/CodeVerifier.kt
index 6dacb90..085aaee 100644
--- a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/CodeVerifier.kt
+++ b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/CodeVerifier.kt
@@ -22,13 +22,14 @@
import java.security.SecureRandom
import java.util.Base64
+/* ktlint-disable max-line-length */
/**
* Authorisation code verifier.
*
- * * Related specifications:
- * Proof Key for Code Exchange by OAuth Public Clients (RFC 7636).
- * https://tools.ietf.org/html/rfc7636
+ * Related specifications:
+ * [Proof Key for Code Exchange by OAuth Public Clients (RFC 7636).](https://tools.ietf.org/html/rfc7636)
*/
+/* ktlint-enable max-line-length */
@RequiresApi(Build.VERSION_CODES.O)
public class CodeVerifier {
private companion object {
@@ -61,14 +62,14 @@
/**
* The verifier value.
*/
- private var value: String? = null
+ public val value: String
@JvmOverloads
public constructor(
/**
- * It is RECOMMENDED that the output of a suitable random number generator be used to create a
- * 32-octet sequence. The octet sequence is then base64url-encoded to produce a 43-octet URL
- * safe string to use as the code verifier.
+ * It is RECOMMENDED that the output of a suitable random number generator be used to create
+ * a 32-octet sequence. The octet sequence is then base64url-encoded to produce a
+ * 43-octet URL safe string to use as the code verifier.
*/
byteLength: Int = 32
) {
@@ -93,17 +94,13 @@
this.value = value
}
- public fun getValue(): String {
- return value!!
- }
-
internal fun getValueBytes(): ByteArray {
- return value!!.toByteArray(StandardCharsets.UTF_8)
+ return value.toByteArray(StandardCharsets.UTF_8)
}
override fun equals(other: Any?): Boolean {
if (other is CodeVerifier) {
- return other.getValue() == value
+ return other.value == value
}
return false
}
diff --git a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/OAuthRequest.kt b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/OAuthRequest.kt
index 89402f1..8f5f9b3 100644
--- a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/OAuthRequest.kt
+++ b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/OAuthRequest.kt
@@ -23,11 +23,26 @@
import androidx.annotation.RequiresApi
/**
- * The OAuth request to be sent to the server to start the OAuth 2 authentication flow
+ * The OAuth request to be sent to the server to start the OAuth 2 authentication flow.
*/
public class OAuthRequest internal constructor(
- private var packageName: String,
- private val requestUrl: Uri
+ /** The package name of the app sending the auth request. */
+ public val packageName: String,
+
+ /**
+ * The Url of the auth request.
+ *
+ * The request is expected to create a URL with the following format:
+ *
+ * ```
+ * https://authorization-server.com/auth?client_id=XXXXX
+ * &redirect_uri=https://wear.googleapis.com/3p_auth/mypackagename
+ * &response_type=code
+ * &code_challenge=XXXXX...XXX
+ * &code_challenge_method=S256
+ * ```
+ */
+ public val requestUrl: Uri
) {
public companion object {
/**
@@ -134,7 +149,7 @@
appendQueryParameter(
requestUriBuilder,
"code_challenge",
- codeChallenge!!.getValue()
+ it.value
)
appendQueryParameter(requestUriBuilder, "code_challenge_method", "S256")
}
@@ -200,20 +215,6 @@
}
}
- /** Get the package name of the app that send the auth request */
- public fun getPackageName(): String = packageName
-
- /**
- * Get the Url of the auth request.
- * The request is expected to craft a URL something like:
- * https://authorization-server.com/auth?client_id=XXXXX
- * &redirect_uri=https://wear.googleapis.com/3p_auth/mypackagename
- * &response_type=code
- * &code_challenge=XXXXX...XXX
- * &code_challenge_method=S256
- */
- public fun getRequestUrl(): Uri = requestUrl
-
internal fun toBundle(): Bundle = Bundle().apply {
putParcelable(RemoteAuthClient.KEY_REQUEST_URL, requestUrl)
putString(RemoteAuthClient.KEY_PACKAGE_NAME, packageName)
diff --git a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/OAuthResponse.kt b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/OAuthResponse.kt
index 7f0e5b4..426bf31 100644
--- a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/OAuthResponse.kt
+++ b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/OAuthResponse.kt
@@ -17,24 +17,28 @@
package androidx.wear.phone.interactions.authentication
import android.net.Uri
+import androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion.ErrorCode
/**
* The authentication response to be sent back to the client after completing the OAuth2 flow.
*/
public class OAuthResponse internal constructor(
- @RemoteAuthClient.Companion.ErrorCode private val errorCode: Int = RemoteAuthClient.NO_ERROR,
- private val responseUrl: Uri?
+ /** The error code that indicated the request result status. */
+ @ErrorCode public val errorCode: Int = RemoteAuthClient.NO_ERROR,
+
+ /** The Url of the auth response. In case of an error it will be null. */
+ public val responseUrl: Uri?
) {
/**
* Builder for constructing new instance of authentication response.
*/
public class Builder {
- @RemoteAuthClient.Companion.ErrorCode
+ @ErrorCode
private var errorCode: Int = RemoteAuthClient.NO_ERROR
private var responseUrl: Uri? = null
/** Set the error code to indicate the request result status */
- public fun setErrorCode(@RemoteAuthClient.Companion.ErrorCode errorCode: Int): Builder =
+ public fun setErrorCode(@ErrorCode errorCode: Int): Builder =
this.apply {
this.errorCode = errorCode
}
@@ -47,11 +51,4 @@
/** Build the response instance specified by this builder*/
public fun build(): OAuthResponse = OAuthResponse(errorCode, responseUrl)
}
-
- /** get the error code that indicated the request result status */
- @RemoteAuthClient.Companion.ErrorCode
- public fun getErrorCode(): Int = errorCode
-
- /** get the Url of the auth response */
- public fun getResponseUrl(): Uri? = responseUrl
-}
\ No newline at end of file
+}
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..bf763b7 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
@@ -41,13 +41,8 @@
* ```
* // PKCE (Proof Key for Code Exchange) is required for the auth
* private var codeVerifier: CodeVerifier
- * private var authClient: RemoteAuthClient
- *
- * override public fun onCreate(b: Bundle) {
- * super.onCreate(b);
- * authClient = RemoteAuthClient.create(this);
- * ...
- * }
+ * // Late initialization in place where it's used, or to be initialized in onCreate()
+ * private var lateinit authClient: RemoteAuthClient
*
* override public fun onDestroy() {
* authClient.close();
@@ -60,11 +55,13 @@
* codeVerifier = CodeVerifier()
*
* // Construct your auth request.
+ * authClient = RemoteAuthClient.create(this);
* authClient.sendAuthorizationRequest(
* OAuthRequest.Builder(this.applicationContext.packageName)
* .setAuthProviderUrl(Uri.parse("https://...."))
* .setCodeChallenge(CodeChallenge(codeVerifier))
* .build(),
+ * Executors.newSingleThreadExecutor()
* new MyAuthCallback()
* );
* }
@@ -80,7 +77,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 +202,7 @@
* see [sendAuthorizationRequest]
*/
@UiThread
- public abstract fun onAuthorizationError(@ErrorCode errorCode: Int)
+ public abstract fun onAuthorizationError(request: OAuthRequest, @ErrorCode errorCode: Int)
}
/**
@@ -215,32 +212,35 @@
* 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) {
- require(packageName == request.getPackageName()) {
+ public fun sendAuthorizationRequest(
+ request: OAuthRequest,
+ executor: Executor,
+ clientCallback: Callback
+ ) {
+ require(packageName == request.packageName) {
"The request's package name is different from the auth client's package name."
}
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 +320,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
@@ -340,14 +341,18 @@
@SuppressLint("SyntheticAccessor")
private fun onResult(response: OAuthResponse) {
- @ErrorCode val error = response.getErrorCode()
+ @ErrorCode val error = response.errorCode
uiThreadExecutor.execute(
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.errorCode)
+ }
}
}
)
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..3a2dff2 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.
@@ -109,8 +109,8 @@
internal fun buildBundleFromResponse(response: OAuthResponse, packageName: String): Bundle =
Bundle().apply {
- putParcelable(KEY_RESPONSE_URL, response.getResponseUrl())
- putInt(KEY_ERROR_CODE, response.getErrorCode())
+ putParcelable(KEY_RESPONSE_URL, response.responseUrl)
+ putInt(KEY_ERROR_CODE, response.errorCode)
putString(KEY_PACKAGE_NAME, packageName)
}
}
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/CodeVerifierCodeChallengeTest.kt b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/CodeVerifierCodeChallengeTest.kt
index 7fb442d..e92e749 100644
--- a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/CodeVerifierCodeChallengeTest.kt
+++ b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/CodeVerifierCodeChallengeTest.kt
@@ -27,13 +27,13 @@
@Test
public fun testVerifierDefaultConstructor() {
val verifier = CodeVerifier()
- assertEquals(43, verifier.getValue().length)
+ assertEquals(43, verifier.value.length)
}
@Test
public fun testVerifierConstructor() {
val verifier = CodeVerifier(96)
- assertEquals(128, verifier.getValue().length)
+ assertEquals(128, verifier.value.length)
}
@Test
@@ -49,7 +49,7 @@
@Test
public fun testVerifierEquality() {
val verifier = CodeVerifier()
- assertTrue(verifier.equals(CodeVerifier(verifier.getValue())))
+ assertTrue(verifier.equals(CodeVerifier(verifier.value)))
}
@Test
@@ -64,7 +64,7 @@
// see https://tools.ietf.org/html/rfc7636#appendix-A
val verifier = CodeVerifier("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk")
val challenge = CodeChallenge(verifier)
- assertEquals("E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", challenge.getValue())
+ assertEquals("E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", challenge.value)
}
@Test
diff --git a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/OAuthRequestResponseTest.kt b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/OAuthRequestResponseTest.kt
index cbbaa46..d708b93 100644
--- a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/OAuthRequestResponseTest.kt
+++ b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/OAuthRequestResponseTest.kt
@@ -53,7 +53,7 @@
fail("The build shall succeed and this line will not be executed")
}
- val requestUrl = request!!.getRequestUrl()
+ val requestUrl = request!!.requestUrl
assertEquals(requestUrl.toString().indexOf(expectedAuthProviderUrl), 0)
assertEquals(requestUrl.getQueryParameter("redirect_uri"), expectedRedirectUri)
assertEquals(requestUrl.getQueryParameter("client_id"), expectedClientId)
@@ -84,7 +84,7 @@
authProviderUrl,
clientId,
redirectUrlWithPackageName,
- codeChallenge.getValue()
+ codeChallenge.value
)
}
@@ -102,7 +102,7 @@
authProviderUrl,
clientId,
customRedirectUrlWithPackageName,
- codeChallenge.getValue()
+ codeChallenge.value
)
}
@@ -115,7 +115,7 @@
"$authProviderUrl?client_id=$clientId" +
"&redirect_uri=$redirectUrlWithPackageName" +
"&response_type=code" +
- "&code_challenge=${codeChallenge.getValue()}" +
+ "&code_challenge=${codeChallenge.value}" +
"&code_challenge_method=S256"
)
)
@@ -125,7 +125,7 @@
authProviderUrl,
clientId,
redirectUrlWithPackageName,
- codeChallenge.getValue()
+ codeChallenge.value
)
}
@@ -187,7 +187,7 @@
authProviderUrl,
clientId,
redirectUrlWithPackageName,
- codeChallenge.getValue()
+ codeChallenge.value
)
}
@@ -217,7 +217,7 @@
checkBuildFailure(
builder,
"The 'code_challenge' query param already exists in the authProviderUrl, " +
- "expect to have the value of '${codeChallenge.getValue()}', but " +
+ "expect to have the value of '${codeChallenge.value}', but " +
"'XXX' is given. Please correct it, or leave it out " +
"to allow the request builder to append it automatically."
)
@@ -228,7 +228,7 @@
val codeChallenge = CodeChallenge(CodeVerifier())
val builder = OAuthRequest.Builder(appPackageName)
.setAuthProviderUrl(
- Uri.parse("$authProviderUrl?code_challenge=${codeChallenge.getValue()}")
+ Uri.parse("$authProviderUrl?code_challenge=${codeChallenge.value}")
)
.setClientId(clientId)
.setCodeChallenge(codeChallenge)
@@ -238,7 +238,7 @@
authProviderUrl,
clientId,
redirectUrlWithPackageName,
- codeChallenge.getValue()
+ codeChallenge.value
)
}
@@ -275,7 +275,7 @@
authProviderUrl,
clientId,
redirectUrlWithPackageName,
- codeChallenge.getValue()
+ codeChallenge.value
)
}
@@ -312,7 +312,7 @@
authProviderUrl,
clientId,
redirectUrlWithPackageName,
- codeChallenge.getValue()
+ codeChallenge.value
)
}
@@ -340,8 +340,8 @@
public fun testNoErrorResponseBuild() {
val response = OAuthResponse.Builder().setResponseUrl(responseUrl).build()
- assertEquals(RemoteAuthClient.NO_ERROR, response.getErrorCode())
- assertEquals(responseUrl, response.getResponseUrl())
+ assertEquals(RemoteAuthClient.NO_ERROR, response.errorCode)
+ assertEquals(responseUrl, response.responseUrl)
}
@Test
@@ -350,14 +350,14 @@
.setErrorCode(RemoteAuthClient.ERROR_UNSUPPORTED)
.build()
- assertEquals(RemoteAuthClient.ERROR_UNSUPPORTED, response1.getErrorCode())
- assertEquals(null, response1.getResponseUrl())
+ assertEquals(RemoteAuthClient.ERROR_UNSUPPORTED, response1.errorCode)
+ assertEquals(null, response1.responseUrl)
val response2 = OAuthResponse.Builder()
.setErrorCode(RemoteAuthClient.ERROR_PHONE_UNAVAILABLE)
.build()
- assertEquals(RemoteAuthClient.ERROR_PHONE_UNAVAILABLE, response2.getErrorCode())
- assertEquals(null, response2.getResponseUrl())
+ assertEquals(RemoteAuthClient.ERROR_PHONE_UNAVAILABLE, response2.errorCode)
+ assertEquals(null, response2.responseUrl)
}
}
\ No newline at end of file
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..f93f2aa 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
@@ -54,8 +54,7 @@
.setAuthProviderUrl(Uri.parse(authProviderUrlB))
.setCodeChallenge(CodeChallenge(CodeVerifier()))
.build()
- private val response =
- OAuthResponse.Builder().setResponseUrl(responseUrl).build()
+ private val response = OAuthResponse.Builder().setResponseUrl(responseUrl).build()
// Note: This can't be static as Robolectric isn't set up at class init time.
private val mServiceName = ComponentName(
@@ -69,6 +68,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 +86,7 @@
.setAuthProviderUrl(Uri.parse(requestUri))
.setCodeChallenge(CodeChallenge(CodeVerifier()))
.build(),
+ executor,
mockCallback
)
// THEN a connection is made to Clockwork Home's Auth service
@@ -95,18 +96,18 @@
@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]
val requestReceived = request.first
// THEN the request url is set correctly
Assert.assertEquals(
- requestA.getRequestUrl(),
- requestReceived.getRequestUrl()
+ requestA.requestUrl,
+ requestReceived.requestUrl
)
Assert.assertEquals(
- requestReceived.getRequestUrl().toString().indexOf(authProviderUrlA),
+ requestReceived.requestUrl.toString().indexOf(authProviderUrlA),
0
)
}
@@ -114,8 +115,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
@@ -124,19 +125,19 @@
Assert.assertEquals(2, fakeService.requests.size.toLong())
// THEN the request url is set correctly for both (A then B)
Assert.assertEquals(
- requestA.getRequestUrl(),
- requestAReceived.getRequestUrl()
+ requestA.requestUrl,
+ requestAReceived.requestUrl
)
Assert.assertEquals(
- requestB.getRequestUrl(),
- requestBReceived.getRequestUrl()
+ requestB.requestUrl,
+ requestBReceived.requestUrl
)
Assert.assertEquals(
- requestAReceived.getRequestUrl().toString().indexOf(authProviderUrlA),
+ requestAReceived.requestUrl.toString().indexOf(authProviderUrlA),
0
)
Assert.assertEquals(
- requestBReceived.getRequestUrl().toString().indexOf(authProviderUrlB),
+ requestBReceived.requestUrl.toString().indexOf(authProviderUrlB),
0
)
}
@@ -145,7 +146,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 +158,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 +176,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 +272,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/androidTest/java/androidx/wear/widget/ConfirmationOverlayTest.java b/wear/wear/src/androidTest/java/androidx/wear/widget/ConfirmationOverlayTest.java
index 1cd7644..2063f3f 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/ConfirmationOverlayTest.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/ConfirmationOverlayTest.java
@@ -29,7 +29,6 @@
import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.FlakyTest;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.wear.R;
@@ -75,7 +74,6 @@
}
@Test
- @FlakyTest(bugId = 190194611)
public void testDefaults_onActivity() throws Throwable {
final CountDownLatch latch = new CountDownLatch(1);
final ConfirmationOverlay overlay = new ConfirmationOverlay();
@@ -118,7 +116,6 @@
}
@Test
- @FlakyTest(bugId = 190194611)
public void testSuccess_onActivity() throws Throwable {
final CountDownLatch latch = new CountDownLatch(1);
final ConfirmationOverlay overlay = new ConfirmationOverlay()
@@ -150,7 +147,6 @@
}
@Test
- @FlakyTest(bugId = 190194611)
public void testFailure_onActivity() throws Throwable {
final CountDownLatch latch = new CountDownLatch(1);
final ConfirmationOverlay overlay = new ConfirmationOverlay()
@@ -182,7 +178,6 @@
}
@Test
- @FlakyTest(bugId = 190194611)
public void testOpenOnPhone_onActivity() throws Throwable {
final CountDownLatch latch = new CountDownLatch(1);
final ConfirmationOverlay overlay = new ConfirmationOverlay()
@@ -255,7 +250,6 @@
}
@Test
- @FlakyTest(bugId = 190194611)
public void testOverlayHiddenAfterSpecifiedDuration() throws Throwable {
final CountDownLatch latch = new CountDownLatch(1);
final int overlayDurationMillis = 2000;
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-samples/build.gradle b/window/window-samples/build.gradle
index c9960e0..3f61782 100644
--- a/window/window-samples/build.gradle
+++ b/window/window-samples/build.gradle
@@ -37,6 +37,7 @@
api(libs.constraintLayout)
// TODO(b/152245564) Conflicting dependencies cause IDE errors.
implementation("androidx.lifecycle:lifecycle-viewmodel:2.2.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-alpha02")
implementation(project(":window:window"))
}
diff --git a/window/window-samples/src/main/java/androidx/window/sample/BaseSampleActivity.kt b/window/window-samples/src/main/java/androidx/window/sample/BaseSampleActivity.kt
deleted file mode 100644
index cf7126b..0000000
--- a/window/window-samples/src/main/java/androidx/window/sample/BaseSampleActivity.kt
+++ /dev/null
@@ -1,55 +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.window.sample
-
-import android.annotation.SuppressLint
-import android.os.Handler
-import android.os.Looper
-import androidx.appcompat.app.AppCompatActivity
-import androidx.window.WindowBackend
-import androidx.window.sample.backend.MidScreenFoldBackend
-import androidx.window.sample.backend.MidScreenFoldBackend.FoldAxis.LONG_DIMENSION
-import androidx.window.sample.backend.MidScreenFoldBackend.FoldAxis.SHORT_DIMENSION
-import java.util.concurrent.Executor
-
-/**
- * Base class for Activities in the samples that allows specifying the [WindowBackend]
- * that should be used by the sample activities in this package. This allows switching between the
- * default backend provided on the device and a test backend (e.g. if the device doesn't provide
- * any).
- */
-@SuppressLint("Registered")
-open class BaseSampleActivity : AppCompatActivity() {
- companion object {
- const val BACKEND_TYPE_EXTRA = "backend_type"
-
- const val BACKEND_TYPE_DEVICE_DEFAULT = 0
- const val BACKEND_TYPE_SHORT_DIMENSION_FOLD = 1
- const val BACKEND_TYPE_LONG_DIMENSION_FOLD = 2
- }
-
- private val handler = Handler(Looper.getMainLooper())
- val mainThreadExecutor = Executor { r: Runnable -> handler.post(r) }
-
- fun getTestBackend(): WindowBackend? {
- return when (intent.getIntExtra(BACKEND_TYPE_EXTRA, BACKEND_TYPE_DEVICE_DEFAULT)) {
- BACKEND_TYPE_SHORT_DIMENSION_FOLD -> MidScreenFoldBackend(SHORT_DIMENSION)
- BACKEND_TYPE_LONG_DIMENSION_FOLD -> MidScreenFoldBackend(LONG_DIMENSION)
- else -> null
- }
- }
-}
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/DisplayFeaturesActivity.kt b/window/window-samples/src/main/java/androidx/window/sample/DisplayFeaturesActivity.kt
index 7dee369..b54d22c 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/DisplayFeaturesActivity.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/DisplayFeaturesActivity.kt
@@ -21,49 +21,53 @@
import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
-import androidx.core.util.Consumer
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.window.WindowInfoRepo
import androidx.window.WindowLayoutInfo
-import androidx.window.WindowManager
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/** Demo activity that shows all display features and current device state on the screen. */
-class DisplayFeaturesActivity : BaseSampleActivity() {
+class DisplayFeaturesActivity : AppCompatActivity() {
- private lateinit var windowManager: WindowManager
private val stateLog: StringBuilder = StringBuilder()
private val displayFeatureViews = ArrayList<View>()
- // Store most recent values for the device state and window layout
- private val stateContainer = StateContainer()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_display_features)
- windowManager = getTestBackend()?.let { backend ->
- @Suppress("DEPRECATION") // TODO(b/173739071) remove when updating WindowManager
- WindowManager(this, backend)
- }
- ?: WindowManager(this)
+ val windowInfoRepo = WindowInfoRepo.create(this)
+ lifecycleScope.launch(Dispatchers.Main) {
+ // The block passed to repeatOnLifecycle is executed when the lifecycle
+ // is at least STARTED and is cancelled when the lifecycle is STOPPED.
+ // It automatically restarts the block when the lifecycle is STARTED again.
+ lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ // Safely collect from windowInfoRepo when the lifecycle is STARTED
+ // and stops collection when the lifecycle is STOPPED
+ windowInfoRepo.windowLayoutInfo
+ .collect { newLayoutInfo ->
+ // New posture information
+ updateStateLog(newLayoutInfo)
+ updateCurrentState(newLayoutInfo)
+ }
+ }
+ }
stateLog.clear()
stateLog.append(getString(R.string.stateUpdateLog)).append("\n")
}
- override fun onStart() {
- super.onStart()
- windowManager.registerLayoutChangeCallback(mainThreadExecutor, stateContainer)
- }
-
- override fun onStop() {
- super.onStop()
- windowManager.unregisterLayoutChangeCallback(stateContainer)
- }
-
/** Updates the device state and display feature positions. */
- internal fun updateCurrentState() {
+ internal fun updateCurrentState(windowLayoutInfo: WindowLayoutInfo) {
// Cleanup previously added feature views
val rootLayout = findViewById<FrameLayout>(R.id.featureContainerLayout)
for (featureView in displayFeatureViews) {
@@ -74,33 +78,30 @@
// Update the UI with the current state
val stateStringBuilder = StringBuilder()
- stateContainer.lastLayoutInfo?.let { windowLayoutInfo ->
- stateStringBuilder.append(getString(R.string.windowLayout))
- .append(": ")
- .append(windowLayoutInfo)
+ stateStringBuilder.append(getString(R.string.windowLayout))
+ .append(": ")
- // Add views that represent display features
- for (displayFeature in windowLayoutInfo.displayFeatures) {
- val lp = getLayoutParamsForFeatureInFrameLayout(displayFeature, rootLayout)
- ?: continue
+ // Add views that represent display features
+ for (displayFeature in windowLayoutInfo.displayFeatures) {
+ val lp = getLayoutParamsForFeatureInFrameLayout(displayFeature, rootLayout)
+ ?: continue
- // Make sure that zero-wide and zero-high features are still shown
- if (lp.width == 0) {
- lp.width = 1
- }
- if (lp.height == 0) {
- lp.height = 1
- }
-
- val featureView = View(this)
- val color = getColor(R.color.colorFeatureFold)
- featureView.foreground = ColorDrawable(color)
-
- rootLayout.addView(featureView, lp)
- featureView.id = View.generateViewId()
-
- displayFeatureViews.add(featureView)
+ // Make sure that zero-wide and zero-high features are still shown
+ if (lp.width == 0) {
+ lp.width = 1
}
+ if (lp.height == 0) {
+ lp.height = 1
+ }
+
+ val featureView = View(this)
+ val color = getColor(R.color.colorFeatureFold)
+ featureView.foreground = ColorDrawable(color)
+
+ rootLayout.addView(featureView, lp)
+ featureView.id = View.generateViewId()
+
+ displayFeatureViews.add(featureView)
}
findViewById<TextView>(R.id.currentState).text = stateStringBuilder.toString()
@@ -120,14 +121,4 @@
val currentDate = sdf.format(Date())
return currentDate.toString()
}
-
- inner class StateContainer : Consumer<WindowLayoutInfo> {
- var lastLayoutInfo: WindowLayoutInfo? = null
-
- override fun accept(newLayoutInfo: WindowLayoutInfo) {
- updateStateLog(newLayoutInfo)
- lastLayoutInfo = newLayoutInfo
- updateCurrentState()
- }
- }
}
diff --git a/window/window-samples/src/main/java/androidx/window/sample/PresentationActivity.kt b/window/window-samples/src/main/java/androidx/window/sample/PresentationActivity.kt
index 43278c3..12c9b92 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/PresentationActivity.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/PresentationActivity.kt
@@ -27,40 +27,47 @@
import android.view.View
import android.widget.TextView
import android.widget.Toast
-import androidx.core.util.Consumer
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
import androidx.window.FoldingFeature
+import androidx.window.WindowInfoRepo
import androidx.window.WindowLayoutInfo
-import androidx.window.WindowManager
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
/**
* Demo activity that reacts to foldable device state change and shows content on the outside
* display when the device is folded.
*/
-class PresentationActivity : BaseSampleActivity() {
+class PresentationActivity : AppCompatActivity() {
private val TAG = "FoldablePresentation"
- private lateinit var windowManager: WindowManager
- private val deviceStateChangeCallback = WindowLayoutInfoChangeCallback()
+ private lateinit var windowInfoRepo: WindowInfoRepo
private var presentation: DemoPresentation? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_foldin)
- windowManager = getTestBackend()?.let { backend ->
- @Suppress("DEPRECATION") // TODO(b/173739071) remove when updating WindowManager
- WindowManager(this, backend)
- }
- ?: WindowManager(this)
- windowManager.registerLayoutChangeCallback(
- mainThreadExecutor,
- deviceStateChangeCallback
- )
- }
+ windowInfoRepo = WindowInfoRepo.create(this)
- override fun onDestroy() {
- super.onDestroy()
- windowManager.unregisterLayoutChangeCallback(deviceStateChangeCallback)
+ lifecycleScope.launch(Dispatchers.Main) {
+ // The block passed to repeatOnLifecycle is executed when the lifecycle
+ // is at least STARTED and is cancelled when the lifecycle is STOPPED.
+ // It automatically restarts the block when the lifecycle is STARTED again.
+ lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ // Safely collect from windowInfoRepo when the lifecycle is STARTED
+ // and stops collection when the lifecycle is STOPPED
+ windowInfoRepo.windowLayoutInfo
+ .collect { newLayoutInfo ->
+ // New posture information
+ updateCurrentState(newLayoutInfo)
+ }
+ }
+ }
}
internal fun startPresentation(context: Context) {
@@ -155,10 +162,4 @@
findViewById<TextView>(R.id.currentState).text = stateStringBuilder.toString()
}
-
- inner class WindowLayoutInfoChangeCallback : Consumer<WindowLayoutInfo> {
- override fun accept(info: WindowLayoutInfo) {
- updateCurrentState(info)
- }
- }
}
diff --git a/window/window-samples/src/main/java/androidx/window/sample/SplitLayoutActivity.kt b/window/window-samples/src/main/java/androidx/window/sample/SplitLayoutActivity.kt
index b8f3d68..e4e99e8 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/SplitLayoutActivity.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/SplitLayoutActivity.kt
@@ -17,40 +17,40 @@
package androidx.window.sample
import android.os.Bundle
-import androidx.core.util.Consumer
-import androidx.window.WindowLayoutInfo
-import androidx.window.WindowManager
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.window.WindowInfoRepo
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
/** Demo of [SplitLayout]. */
-class SplitLayoutActivity : BaseSampleActivity() {
+class SplitLayoutActivity : AppCompatActivity() {
- private lateinit var windowManager: WindowManager
- private val layoutStateChangeCallback = LayoutStateChangeCallback()
+ private lateinit var windowInfoRepo: WindowInfoRepo
+ private lateinit var splitLayout: SplitLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_split_layout)
- windowManager = getTestBackend()?.let { backend ->
- @Suppress("DEPRECATION") // TODO(b/173739071) remove when updating WindowManager
- WindowManager(this, backend)
- }
- ?: WindowManager(this)
- }
+ windowInfoRepo = WindowInfoRepo.create(this)
+ splitLayout = findViewById(R.id.split_layout)
- override fun onStart() {
- super.onStart()
- windowManager.registerLayoutChangeCallback(mainThreadExecutor, layoutStateChangeCallback)
- }
-
- override fun onStop() {
- super.onStop()
- windowManager.unregisterLayoutChangeCallback(layoutStateChangeCallback)
- }
-
- inner class LayoutStateChangeCallback : Consumer<WindowLayoutInfo> {
- override fun accept(newLayoutInfo: WindowLayoutInfo) {
- val splitLayout = findViewById<SplitLayout>(R.id.split_layout)
- splitLayout.updateWindowLayout(newLayoutInfo)
+ // Create a new coroutine since repeatOnLifecycle is a suspend function
+ lifecycleScope.launch(Dispatchers.Main) {
+ // The block passed to repeatOnLifecycle is executed when the lifecycle
+ // is at least STARTED and is cancelled when the lifecycle is STOPPED.
+ // It automatically restarts the block when the lifecycle is STARTED again.
+ lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ // Safely collect from windowInfoRepo when the lifecycle is STARTED
+ // and stops collection when the lifecycle is STOPPED
+ windowInfoRepo.windowLayoutInfo
+ .collect { newLayoutInfo ->
+ splitLayout.updateWindowLayout(newLayoutInfo)
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/WindowDemosActivity.kt b/window/window-samples/src/main/java/androidx/window/sample/WindowDemosActivity.kt
index ad02757..77bc954 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/WindowDemosActivity.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/WindowDemosActivity.kt
@@ -19,71 +19,30 @@
import android.content.Intent
import android.os.Bundle
import android.view.View
-import android.widget.RadioGroup
import androidx.appcompat.app.AppCompatActivity
-import androidx.window.sample.BaseSampleActivity.Companion.BACKEND_TYPE_DEVICE_DEFAULT
-import androidx.window.sample.BaseSampleActivity.Companion.BACKEND_TYPE_EXTRA
-import androidx.window.sample.BaseSampleActivity.Companion.BACKEND_TYPE_LONG_DIMENSION_FOLD
-import androidx.window.sample.BaseSampleActivity.Companion.BACKEND_TYPE_SHORT_DIMENSION_FOLD
/**
- * Main activity that launches WindowManager demos. Allows the user to choose the backend to use
- * with the [androidx.window.WindowManager] library interface, which can be helpful if the test
- * device does not report any display features.
- */
+ * Main activity that launches WindowManager demos.
+*/
class WindowDemosActivity : AppCompatActivity() {
- private var selectedBackend = BACKEND_TYPE_DEVICE_DEFAULT
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_window_demos)
-
- val radioGroup = findViewById<RadioGroup>(R.id.backendRadioGroup)
- radioGroup.setOnCheckedChangeListener { _, checkedId ->
- selectedBackend = when (checkedId) {
- R.id.deviceDefaultRadioButton -> BACKEND_TYPE_DEVICE_DEFAULT
- R.id.shortDimensionFoldRadioButton -> BACKEND_TYPE_SHORT_DIMENSION_FOLD
- R.id.longDimensionFoldRadioButton -> BACKEND_TYPE_LONG_DIMENSION_FOLD
- else -> BACKEND_TYPE_DEVICE_DEFAULT
- }
- }
-
- if (savedInstanceState != null) {
- selectedBackend = savedInstanceState.getInt(
- BACKEND_TYPE_EXTRA,
- BACKEND_TYPE_DEVICE_DEFAULT
- )
- }
- when (selectedBackend) {
- BACKEND_TYPE_DEVICE_DEFAULT ->
- radioGroup.check(R.id.deviceDefaultRadioButton)
- BACKEND_TYPE_SHORT_DIMENSION_FOLD ->
- radioGroup.check(R.id.shortDimensionFoldRadioButton)
- BACKEND_TYPE_LONG_DIMENSION_FOLD ->
- radioGroup.check(R.id.longDimensionFoldRadioButton)
- }
- }
-
- override fun onSaveInstanceState(outState: Bundle) {
- super.onSaveInstanceState(outState)
- outState.putInt(BACKEND_TYPE_EXTRA, selectedBackend)
}
fun showDisplayFeatures(@Suppress("UNUSED_PARAMETER")view: View) {
val intent = Intent(this, DisplayFeaturesActivity::class.java)
- intent.putExtra(BACKEND_TYPE_EXTRA, selectedBackend)
startActivity(intent)
}
fun showSplitLayout(@Suppress("UNUSED_PARAMETER")view: View) {
val intent = Intent(this, SplitLayoutActivity::class.java)
- intent.putExtra(BACKEND_TYPE_EXTRA, selectedBackend)
startActivity(intent)
}
fun showPresentation(@Suppress("UNUSED_PARAMETER")view: View) {
val intent = Intent(this, PresentationActivity::class.java)
- intent.putExtra(BACKEND_TYPE_EXTRA, selectedBackend)
startActivity(intent)
}
}
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/backend/ActivityExtensions.kt b/window/window-samples/src/main/java/androidx/window/sample/backend/ActivityExtensions.kt
deleted file mode 100644
index ad48750..0000000
--- a/window/window-samples/src/main/java/androidx/window/sample/backend/ActivityExtensions.kt
+++ /dev/null
@@ -1,30 +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.window.sample.backend
-
-import android.app.Activity
-import android.graphics.Point
-import androidx.window.WindowManager
-
-/**
- * Return a [Point] whose dimensions match the metrics of the window.
- * @return [Point] whose dimensions match the metrics of the window.
- */
-internal fun Activity.calculateWindowSizeExt(): Point {
- val bounds = WindowManager(this).getCurrentWindowMetrics().bounds
- return Point(bounds.width(), bounds.height())
-}
diff --git a/window/window-samples/src/main/java/androidx/window/sample/backend/MidScreenFoldBackend.kt b/window/window-samples/src/main/java/androidx/window/sample/backend/MidScreenFoldBackend.kt
deleted file mode 100644
index 04a929f..0000000
--- a/window/window-samples/src/main/java/androidx/window/sample/backend/MidScreenFoldBackend.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION") // TODO(b/173739071) Remove DeviceState
-
-package androidx.window.sample.backend
-
-import android.app.Activity
-import android.graphics.Point
-import android.graphics.Rect
-import androidx.core.util.Consumer
-import androidx.window.DisplayFeature
-import androidx.window.FoldingFeature
-import androidx.window.WindowBackend
-import androidx.window.WindowLayoutInfo
-import java.util.concurrent.Executor
-
-/**
- * A sample backend that will have a fold in the middle of the screen. The {@link FoldAxis}
- * specifies which axis is followed. This sample backend can be used to model devices that open
- * like a clam shell or a book. This is relative to the display's dimensions. For displays that
- * are taller than they are wide with a short fold axis they will mimic a horizontal fold. For
- * displays that are wider than they are tall with a short fold axis will mimic a vertical fold.
- *
- * <p>The {@link DeviceState} is fixed to have an opened posture. This sample implementation
- * ignores the device state listener. Combine with {@link InitialValueWindowBackendDecorator} to
- * receive the initial value through the listener.
- *
- * <p>The {@link WindowLayoutInfo} is also fixed to have a {@link TYPE_FOLD} where the longest
- * dimension runs parallel to the specified {@link FoldAxis}. The fold is placed in the middle
- * of the display. Like the {@link DeviceState}, the sample implementation ignores the window
- * layout info listener. Combine with {@link InitialValueWindowBackendDecorator} to
- * receive the initial value through the listener.
- */
-class MidScreenFoldBackend(private val foldAxis: FoldAxis) : WindowBackend {
- /**
- * The side which the fold axis should be parallel to.
- */
- enum class FoldAxis {
- LONG_DIMENSION,
- SHORT_DIMENSION
- }
-
- /**
- * @param activity Currently running {@link Activity}.
- * @return A fake {@link WindowLayoutInfo} with a fold in the middle matching the {@link
- * FoldAxis}.
- */
- private fun getWindowLayoutInfo(activity: Activity): WindowLayoutInfo {
- val windowSize = activity.calculateWindowSizeExt()
- val featureRect = foldRect(windowSize)
-
- val displayFeature =
- FoldingFeature(
- featureRect,
- FoldingFeature.Type.FOLD,
- FoldingFeature.State.FLAT
- )
- val featureList = ArrayList<DisplayFeature>()
- featureList.add(displayFeature)
- return WindowLayoutInfo.Builder().setDisplayFeatures(featureList).build()
- }
-
- private fun foldRect(windowSize: Point): Rect {
- return when (foldAxis) {
- FoldAxis.LONG_DIMENSION -> longDimensionFold(windowSize)
- FoldAxis.SHORT_DIMENSION -> shortDimensionFold(windowSize)
- }
- }
-
- private fun shortDimensionFold(windowSize: Point): Rect {
- return if (windowSize.x >= windowSize.y) { // Landscape
- Rect(windowSize.x / 2, 0, windowSize.x / 2, windowSize.y)
- } else { // Portrait
- Rect(0, windowSize.y / 2, windowSize.x, windowSize.y / 2)
- }
- }
-
- private fun longDimensionFold(windowSize: Point): Rect {
- return if (windowSize.x >= windowSize.y) { // Landscape
- Rect(0, windowSize.y / 2, windowSize.x, windowSize.y / 2)
- } else { // Portrait
- Rect(windowSize.x / 2, 0, windowSize.x / 2, windowSize.y)
- }
- }
-
- override fun registerLayoutChangeCallback(
- activity: Activity,
- executor: Executor,
- callback: Consumer<WindowLayoutInfo>
- ) {
- executor.execute { callback.accept(getWindowLayoutInfo(activity)) }
- }
-
- override fun unregisterLayoutChangeCallback(callback: Consumer<WindowLayoutInfo>) {
- }
-}
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/layout/activity_window_demos.xml b/window/window-samples/src/main/res/layout/activity_window_demos.xml
index 03f3926f..bea8045 100644
--- a/window/window-samples/src/main/res/layout/activity_window_demos.xml
+++ b/window/window-samples/src/main/res/layout/activity_window_demos.xml
@@ -28,48 +28,6 @@
android:layout_marginStart="8dp" >
<TextView
- android:id="@+id/hardwareSelectTextView"
- android:layout_width="match_parent"
- android:layout_height="40dp"
- android:layout_marginTop="8dp"
- android:gravity="center_vertical"
- android:text="@string/hardware_config_select"
- android:textSize="20sp"
- android:textStyle="bold"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
-
- <RadioGroup
- android:id="@+id/backendRadioGroup"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginTop="8dp"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/hardwareSelectTextView">
-
- <RadioButton
- android:id="@+id/deviceDefaultRadioButton"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/device_default" />
-
- <RadioButton
- android:id="@+id/shortDimensionFoldRadioButton"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/short_dimension_fold" />
-
- <RadioButton
- android:id="@+id/longDimensionFoldRadioButton"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/long_dimension_fold" />
-
- </RadioGroup>
-
- <TextView
android:id="@+id/testActivityTitle"
android:layout_width="match_parent"
android:layout_height="40dp"
@@ -80,7 +38,7 @@
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/backendRadioGroup" />
+ app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/featuresActivityButton"
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index f8bbe6a..b981262 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -72,11 +72,6 @@
public static final class FoldingFeature.Type.Companion {
}
- public interface WindowBackend {
- method public void registerLayoutChangeCallback(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo> callback);
- method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo> callback);
- }
-
public interface WindowInfoRepo {
method public default static androidx.window.WindowInfoRepo create(android.app.Activity activity);
method public kotlinx.coroutines.flow.Flow<androidx.window.WindowMetrics> getCurrentWindowMetrics();
@@ -103,15 +98,6 @@
method public androidx.window.WindowLayoutInfo.Builder setDisplayFeatures(java.util.List<? extends androidx.window.DisplayFeature> displayFeatures);
}
- public final class WindowManager {
- ctor public WindowManager(android.content.Context context, optional androidx.window.WindowBackend windowBackend);
- ctor public WindowManager(android.content.Context context);
- method public androidx.window.WindowMetrics getCurrentWindowMetrics();
- method public androidx.window.WindowMetrics getMaximumWindowMetrics();
- method public void registerLayoutChangeCallback(java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo> callback);
- method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo> callback);
- }
-
public final class WindowMetrics {
ctor public WindowMetrics(android.graphics.Rect bounds);
method public android.graphics.Rect getBounds();
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index f8bbe6a..b981262 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -72,11 +72,6 @@
public static final class FoldingFeature.Type.Companion {
}
- public interface WindowBackend {
- method public void registerLayoutChangeCallback(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo> callback);
- method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo> callback);
- }
-
public interface WindowInfoRepo {
method public default static androidx.window.WindowInfoRepo create(android.app.Activity activity);
method public kotlinx.coroutines.flow.Flow<androidx.window.WindowMetrics> getCurrentWindowMetrics();
@@ -103,15 +98,6 @@
method public androidx.window.WindowLayoutInfo.Builder setDisplayFeatures(java.util.List<? extends androidx.window.DisplayFeature> displayFeatures);
}
- public final class WindowManager {
- ctor public WindowManager(android.content.Context context, optional androidx.window.WindowBackend windowBackend);
- ctor public WindowManager(android.content.Context context);
- method public androidx.window.WindowMetrics getCurrentWindowMetrics();
- method public androidx.window.WindowMetrics getMaximumWindowMetrics();
- method public void registerLayoutChangeCallback(java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo> callback);
- method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo> callback);
- }
-
public final class WindowMetrics {
ctor public WindowMetrics(android.graphics.Rect bounds);
method public android.graphics.Rect getBounds();
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index 4d7bd79..2a83461 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -72,11 +72,6 @@
public static final class FoldingFeature.Type.Companion {
}
- public interface WindowBackend {
- method public void registerLayoutChangeCallback(android.app.Activity activity, java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo> callback);
- method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo> callback);
- }
-
public interface WindowInfoRepo {
method public default static androidx.window.WindowInfoRepo create(android.app.Activity activity);
method public kotlinx.coroutines.flow.Flow<androidx.window.WindowMetrics> getCurrentWindowMetrics();
@@ -111,15 +106,6 @@
method public androidx.window.WindowLayoutInfo.Builder setDisplayFeatures(java.util.List<? extends androidx.window.DisplayFeature> displayFeatures);
}
- public final class WindowManager {
- ctor public WindowManager(android.content.Context context, optional androidx.window.WindowBackend windowBackend);
- ctor public WindowManager(android.content.Context context);
- method public androidx.window.WindowMetrics getCurrentWindowMetrics();
- method public androidx.window.WindowMetrics getMaximumWindowMetrics();
- method public void registerLayoutChangeCallback(java.util.concurrent.Executor executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo> callback);
- method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo> callback);
- }
-
public final class WindowMetrics {
ctor public WindowMetrics(android.graphics.Rect bounds);
method public android.graphics.Rect getBounds();
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/window/window/src/androidTest/java/androidx/window/ExtensionTest.kt b/window/window/src/androidTest/java/androidx/window/ExtensionTest.kt
index 2a8267c..f3d7c57 100644
--- a/window/window/src/androidTest/java/androidx/window/ExtensionTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/ExtensionTest.kt
@@ -257,7 +257,8 @@
return false
}
val featureRect: Rect = displayFeature.bounds
- val windowMetrics = WindowManager(activity).getCurrentWindowMetrics()
+ val windowMetrics = WindowMetricsCalculator.create()
+ .computeCurrentWindowMetrics(activity)
if (
featureRect.height() == 0 && featureRect.width() == 0 || featureRect.left < 0 ||
featureRect.top < 0
diff --git a/window/window/src/androidTest/java/androidx/window/WindowBackendTest.kt b/window/window/src/androidTest/java/androidx/window/WindowBackendTest.kt
deleted file mode 100644
index 6cad0b7..0000000
--- a/window/window/src/androidTest/java/androidx/window/WindowBackendTest.kt
+++ /dev/null
@@ -1,71 +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.window
-
-import android.app.Activity
-import androidx.core.util.Consumer
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import androidx.window.FoldingFeature.State.Companion.FLAT
-import androidx.window.FoldingFeature.Type.Companion.HINGE
-import com.google.common.util.concurrent.MoreExecutors
-import com.nhaarman.mockitokotlin2.mock
-import com.nhaarman.mockitokotlin2.verify
-import org.junit.Test
-import org.junit.runner.RunWith
-import java.util.concurrent.Executor
-
-/** Tests for [WindowBackend] class. */
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-public class WindowBackendTest : WindowTestBase() {
-
- /**
- * Verifies that [WindowManager] instance would use the assigned
- * [WindowBackend].
- */
- @Test
- public fun testFakeWindowBackend() {
- val windowLayoutInfo = newTestWindowLayout()
- val windowBackend: WindowBackend = FakeWindowBackend(windowLayoutInfo)
- activityTestRule.scenario.onActivity { activity ->
- val wm = WindowManager(activity, windowBackend)
- val layoutInfoConsumer = mock<Consumer<WindowLayoutInfo>>()
- wm.registerLayoutChangeCallback(MoreExecutors.directExecutor(), layoutInfoConsumer)
- verify(layoutInfoConsumer).accept(windowLayoutInfo)
- }
- }
-
- private fun newTestWindowLayout(): WindowLayoutInfo {
- val displayFeature = FoldingFeature(Bounds(10, 0, 10, 100), HINGE, FLAT)
- return WindowLayoutInfo(listOf(displayFeature))
- }
-
- private class FakeWindowBackend(private val windowLayoutInfo: WindowLayoutInfo) :
- WindowBackend {
- override fun registerLayoutChangeCallback(
- activity: Activity,
- executor: Executor,
- callback: Consumer<WindowLayoutInfo>
- ) {
- executor.execute { callback.accept(windowLayoutInfo) }
- }
-
- override fun unregisterLayoutChangeCallback(callback: Consumer<WindowLayoutInfo>) {
- // Empty
- }
- }
-}
diff --git a/window/window/src/androidTest/java/androidx/window/WindowManagerTest.kt b/window/window/src/androidTest/java/androidx/window/WindowManagerTest.kt
deleted file mode 100644
index 9925018..0000000
--- a/window/window/src/androidTest/java/androidx/window/WindowManagerTest.kt
+++ /dev/null
@@ -1,110 +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.window
-
-import android.app.Activity
-import android.content.ContextWrapper
-import android.graphics.Rect
-import androidx.core.util.Consumer
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import com.google.common.util.concurrent.MoreExecutors
-import com.nhaarman.mockitokotlin2.eq
-import com.nhaarman.mockitokotlin2.mock
-import com.nhaarman.mockitokotlin2.verify
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNotNull
-import org.junit.Test
-import org.junit.runner.RunWith
-
-/** Tests for [WindowManager] class. */
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-public class WindowManagerTest : WindowTestBase() {
-
- @Test
- public fun testConstructor_activity() {
- WindowManager(
- mock<Activity>(),
- mock()
- )
- }
-
- @Test
- public fun testConstructor_wrappedActivity() {
- WindowManager(
- ContextWrapper(mock<Activity>()),
- mock()
- )
- }
-
- @Test
- public fun testConstructor_nullWindowBackend() {
- WindowManager(mock<Activity>())
- }
-
- @Test(expected = IllegalArgumentException::class)
- public fun testConstructor_applicationContext() {
- WindowManager(
- ApplicationProvider.getApplicationContext(),
- mock()
- )
- }
-
- @Test
- public fun testRegisterLayoutChangeCallback() {
- val backend = mock<WindowBackend>()
- val activity = mock<Activity>()
- val wm = WindowManager(activity, backend)
- val executor = MoreExecutors.directExecutor()
- val consumer: Consumer<WindowLayoutInfo> = mock<WindowLayoutInfoConsumer>()
- wm.registerLayoutChangeCallback(executor, consumer)
- verify(backend).registerLayoutChangeCallback(activity, executor, consumer)
- wm.unregisterLayoutChangeCallback(consumer)
- verify(backend).unregisterLayoutChangeCallback(eq(consumer))
- }
-
- @Test
- public fun testGetCurrentWindowMetrics() {
- val backend = mock<WindowBackend>()
- val activity = mock<Activity>()
- val windowMetricsCalculator = TestWindowMetricsCalculator()
- val bounds = Rect(1, 2, 3, 4)
- windowMetricsCalculator.setCurrentBoundsForActivity(activity, bounds)
- val wm = WindowManager(activity, backend)
- wm.windowMetricsCalculator = windowMetricsCalculator
- val windowMetrics = wm.getCurrentWindowMetrics()
- assertNotNull(windowMetrics)
- assertEquals(bounds, windowMetrics.bounds)
- }
-
- @Test
- public fun testGetMaximumWindowMetrics() {
- val backend = mock<WindowBackend>()
- val activity = mock<Activity>()
- val bounds = Rect(0, 2, 4, 5)
- val windowMetricsCalculator = TestWindowMetricsCalculator()
- windowMetricsCalculator.setMaximumBoundsForActivity(activity, bounds)
- val wm = WindowManager(activity, backend)
- wm.windowMetricsCalculator = windowMetricsCalculator
- val windowMetrics = wm.getMaximumWindowMetrics()
- assertNotNull(windowMetrics)
- assertEquals(bounds, windowMetrics.bounds)
- }
-
- private interface WindowLayoutInfoConsumer : Consumer<WindowLayoutInfo>
-}
diff --git a/window/window/src/main/java/androidx/window/WindowBackend.kt b/window/window/src/main/java/androidx/window/WindowBackend.kt
index 6f6e351..4f10e72 100644
--- a/window/window/src/main/java/androidx/window/WindowBackend.kt
+++ b/window/window/src/main/java/androidx/window/WindowBackend.kt
@@ -20,15 +20,15 @@
import java.util.concurrent.Executor
/**
- * Backing interface for [WindowManager] instances that serve as the default
+ * Backing interface for [WindowInfoRepo] instances that serve as the default
* information supplier.
*/
-public interface WindowBackend {
+internal interface WindowBackend {
/**
* Registers a callback for layout changes of the window for the supplied [Activity].
* Must be called only after the it is attached to the window.
*/
- public fun registerLayoutChangeCallback(
+ fun registerLayoutChangeCallback(
activity: Activity,
executor: Executor,
callback: Consumer<WindowLayoutInfo>
@@ -37,5 +37,5 @@
/**
* Unregisters a callback for window layout changes of the [Activity] window.
*/
- public fun unregisterLayoutChangeCallback(callback: Consumer<WindowLayoutInfo>)
+ fun unregisterLayoutChangeCallback(callback: Consumer<WindowLayoutInfo>)
}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/WindowLayoutInfo.kt b/window/window/src/main/java/androidx/window/WindowLayoutInfo.kt
index 2c44a3a..e818c56 100644
--- a/window/window/src/main/java/androidx/window/WindowLayoutInfo.kt
+++ b/window/window/src/main/java/androidx/window/WindowLayoutInfo.kt
@@ -22,7 +22,7 @@
*
* Only the features that are present within the current window bounds are reported. Their
* positions and sizes can change if the window is moved or resized on screen.
- * @see WindowManager.registerLayoutChangeCallback
+ * @see WindowInfoRepo.windowLayoutInfo
*/
public class WindowLayoutInfo internal constructor(
/**
diff --git a/window/window/src/main/java/androidx/window/WindowManager.kt b/window/window/src/main/java/androidx/window/WindowManager.kt
deleted file mode 100644
index 6f3b71e..0000000
--- a/window/window/src/main/java/androidx/window/WindowManager.kt
+++ /dev/null
@@ -1,152 +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.window
-
-import android.app.Activity
-import android.content.Context
-import android.content.ContextWrapper
-import androidx.core.util.Consumer
-import java.util.concurrent.Executor
-
-/**
- * Main interaction point with the WindowManager library. An instance of this class allows
- * polling the current state of the device and display, and registering callbacks for changes in
- * the corresponding states.
- *
- * All methods of this class will return information that is associated with this visual
- * context.
- *
- */
-public class WindowManager @JvmOverloads constructor(
- /**
- * A visual [Context], such as an [Activity] or a [ContextWrapper]
- */
- context: Context,
- /**
- * Backing server class that will provide information for this instance.
- *
- * Pass a custom [WindowBackend] implementation for testing.
- */
- private val windowBackend: WindowBackend = ExtensionWindowBackend.getInstance(
- context
- )
-) {
- /**
- * A class to calculate the bounds of a [android.view.Window] across different versions of
- * Android.
- */
- internal var windowMetricsCalculator: WindowMetricsCalculator = WindowMetricsCalculatorCompat
-
- /**
- * Activity that was registered with this instance of [WindowManager] at creation.
- * This is used to find the token identifier of the window when requesting layout information
- * from the [androidx.window.sidecar.SidecarInterface] or is passed directly to the
- * [androidx.window.extensions.ExtensionInterface].
- */
- private val activity: Activity = getActivityFromContext(context)
- ?: throw IllegalArgumentException(
- "Used non-visual Context to obtain an instance of WindowManager. Please use an " +
- "Activity or a ContextWrapper around one instead."
- )
-
- /**
- * Registers a callback for layout changes of the window of the current visual [Context].
- * Must be called only after the it is attached to the window.
- *
- * @see Activity.onAttachedToWindow
- */
- public fun registerLayoutChangeCallback(
- executor: Executor,
- callback: Consumer<WindowLayoutInfo>
- ) {
- windowBackend.registerLayoutChangeCallback(activity, executor, callback)
- }
-
- /**
- * Unregisters a callback for window layout changes of the window.
- */
- public fun unregisterLayoutChangeCallback(callback: Consumer<WindowLayoutInfo>) {
- windowBackend.unregisterLayoutChangeCallback(callback)
- }
-
- /**
- * Returns the [WindowMetrics] according to the current system state.
- *
- *
- * The metrics describe the size of the area the window would occupy with
- * [MATCH_PARENT][android.view.WindowManager.LayoutParams.MATCH_PARENT] width and height
- * and any combination of flags that would allow the window to extend behind display cutouts.
- *
- *
- * The value of this is based on the **current** windowing state of the system. For
- * example, for activities in multi-window mode, the metrics returned are based on the
- * current bounds that the user has selected for the [Activity][android.app.Activity]'s
- * window.
- *
- * @see getMaximumWindowMetrics
- * @see android.view.WindowManager.getCurrentWindowMetrics
- */
- public fun getCurrentWindowMetrics(): WindowMetrics {
- return windowMetricsCalculator.computeCurrentWindowMetrics(activity)
- }
-
- /**
- * Returns the largest [WindowMetrics] an app may expect in the current system state.
- *
- *
- * The metrics describe the size of the largest potential area the window might occupy with
- * [MATCH_PARENT][android.view.WindowManager.LayoutParams.MATCH_PARENT] width and height
- * and any combination of flags that would allow the window to extend behind display cutouts.
- *
- *
- * The value of this is based on the largest **potential** windowing state of the system.
- * For example, for activities in multi-window mode the metrics returned are based on what the
- * bounds would be if the user expanded the window to cover the entire screen.
- *
- *
- * Note that this might still be smaller than the size of the physical display if certain
- * areas of the display are not available to windows created for the associated [Context].
- * For example, devices with foldable displays that wrap around the enclosure may split the
- * physical display into different regions, one for the front and one for the back, each acting
- * as different logical displays. In this case [.getMaximumWindowMetrics] would return
- * the region describing the side of the device the associated [context's][Context]
- * window is placed.
- *
- * @see getCurrentWindowMetrics
- * @see android.view.WindowManager.getMaximumWindowMetrics
- */
- public fun getMaximumWindowMetrics(): WindowMetrics {
- return windowMetricsCalculator.computeMaximumWindowMetrics(activity)
- }
-
- internal companion object {
- /**
- * Unwraps the hierarchy of [ContextWrapper]-s until [Activity] is reached.
- *
- * @return Base [Activity] context or `null` if not available.
- */
- fun getActivityFromContext(initialContext: Context): Activity? {
- var context: Context? = initialContext
- while (context is ContextWrapper) {
- if (context is Activity) {
- return context
- }
- context = context.baseContext
- }
- return null
- }
- }
-}
diff --git a/window/window/src/main/java/androidx/window/WindowMetrics.kt b/window/window/src/main/java/androidx/window/WindowMetrics.kt
index b026004..e86b884 100644
--- a/window/window/src/main/java/androidx/window/WindowMetrics.kt
+++ b/window/window/src/main/java/androidx/window/WindowMetrics.kt
@@ -21,11 +21,11 @@
* Metrics about a [android.view.Window], consisting of its bounds.
*
*
- * This is usually obtained from [WindowManager#getCurrentWindowMetrics] or
- * [WindowManager#getMaximumWindowMetrics].
+ * This is usually obtained from [WindowInfoRepo.currentWindowMetrics] or
+ * [WindowInfoRepo.maximumWindowMetrics].
*
- * @see WindowManager#getCurrentWindowMetrics
- * @see WindowManager#getMaximumWindowMetrics
+ * @see WindowInfoRepo.currentWindowMetrics
+ * @see WindowInfoRepo.maximumWindowMetrics
*/
public class WindowMetrics internal constructor(private val _bounds: Bounds) {
diff --git a/work/workmanager-lint/src/test/java/androidx/work/lint/PeriodicEnqueueIssueDetectorTest.kt b/work/workmanager-lint/src/test/java/androidx/work/lint/PeriodicEnqueueIssueDetectorTest.kt
index 7b4b9f8..abddad8 100644
--- a/work/workmanager-lint/src/test/java/androidx/work/lint/PeriodicEnqueueIssueDetectorTest.kt
+++ b/work/workmanager-lint/src/test/java/androidx/work/lint/PeriodicEnqueueIssueDetectorTest.kt
@@ -16,6 +16,7 @@
package androidx.work.lint
+import androidx.work.lint.Stubs.LISTENABLE_WORKER
import androidx.work.lint.Stubs.ONE_TIME_WORK_REQUEST
import androidx.work.lint.Stubs.PERIODIC_WORK_REQUEST
import androidx.work.lint.Stubs.WORK_MANAGER
@@ -52,6 +53,7 @@
WORK_REQUEST,
ONE_TIME_WORK_REQUEST,
PERIODIC_WORK_REQUEST,
+ LISTENABLE_WORKER,
snippet
).issues(PeriodicEnqueueIssueDetector.ISSUE)
.run()
@@ -92,6 +94,7 @@
WORK_REQUEST,
ONE_TIME_WORK_REQUEST,
PERIODIC_WORK_REQUEST,
+ LISTENABLE_WORKER,
snippet
).issues(PeriodicEnqueueIssueDetector.ISSUE)
.run()
@@ -134,6 +137,7 @@
WORK_REQUEST,
ONE_TIME_WORK_REQUEST,
PERIODIC_WORK_REQUEST,
+ LISTENABLE_WORKER,
snippet
).issues(PeriodicEnqueueIssueDetector.ISSUE)
.run()
@@ -155,6 +159,7 @@
"""
package com.example
+ import androidx.work.OneTimeWorkRequest
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
@@ -173,6 +178,7 @@
WORK_REQUEST,
ONE_TIME_WORK_REQUEST,
PERIODIC_WORK_REQUEST,
+ LISTENABLE_WORKER,
snippet
).issues(PeriodicEnqueueIssueDetector.ISSUE)
.run()
@@ -186,6 +192,7 @@
"""
package com.example
+ import androidx.work.OneTimeWorkRequest
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
@@ -204,6 +211,7 @@
WORK_REQUEST,
ONE_TIME_WORK_REQUEST,
PERIODIC_WORK_REQUEST,
+ LISTENABLE_WORKER,
snippet
).issues(PeriodicEnqueueIssueDetector.ISSUE)
.run()
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();
}
/**