Merge "Clean up coroutines in repeated tests like benchmarks" into androidx-master-dev
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index e683bb6..1159550 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -27,6 +27,7 @@
api(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
api(projectOrArtifact(":savedstate:savedstate"))
api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
+ implementation("androidx.tracing:tracing:1.0.0")
androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
androidTestImplementation(KOTLIN_STDLIB)
diff --git a/activity/activity/src/androidTest/AndroidManifest.xml b/activity/activity/src/androidTest/AndroidManifest.xml
index 8ad02b8..5176eb8 100644
--- a/activity/activity/src/androidTest/AndroidManifest.xml
+++ b/activity/activity/src/androidTest/AndroidManifest.xml
@@ -23,6 +23,7 @@
<activity android:name="androidx.activity.LifecycleComponentActivity"/>
<activity android:name="androidx.activity.EagerOverrideLifecycleComponentActivity"/>
<activity android:name="androidx.activity.LazyOverrideLifecycleComponentActivity"/>
+ <activity android:name="androidx.activity.ReportFullyDrawnActivity"/>
<activity android:name="androidx.activity.ViewModelActivity"/>
<activity android:name="androidx.activity.SavedStateActivity"/>
<activity android:name="androidx.activity.ContentViewActivity"/>
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityReportFullyDrawnTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityReportFullyDrawnTest.kt
new file mode 100644
index 0000000..70e6672
--- /dev/null
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityReportFullyDrawnTest.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.activity
+
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.testutils.withActivity
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class ComponentActivityReportFullyDrawnTest {
+
+ @Test
+ fun testReportFullyDrawn() {
+ with(ActivityScenario.launch(ReportFullyDrawnActivity::class.java)) {
+ withActivity {
+ // This test makes sure that this method does not throw an exception on devices
+ // running API 19 (without UPDATE_DEVICE_STATS permission) and earlier
+ // (regardless or permissions).
+ reportFullyDrawn()
+ }
+ }
+ }
+}
+
+class ReportFullyDrawnActivity : ComponentActivity()
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 0b2f103..afb2ff0 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt
@@ -310,6 +310,41 @@
}
@Test
+ fun testUnregisterAfterSavedState() {
+ val lifecycleOwner = TestLifecycleOwner(Lifecycle.State.INITIALIZED)
+ var resultReturned = false
+ val activityResult = registry.register("key", lifecycleOwner, StartActivityForResult()) { }
+
+ activityResult.launch(null)
+
+ val savedState = Bundle()
+ registry.onSaveInstanceState(savedState)
+
+ registry.unregister("key")
+
+ val restoredRegistry = object : ActivityResultRegistry() {
+ override fun <I : Any?, O : Any?> onLaunch(
+ requestCode: Int,
+ contract: ActivityResultContract<I, O>,
+ input: I,
+ options: ActivityOptionsCompat?
+ ) {
+ dispatchResult(requestCode, RESULT_OK, Intent())
+ }
+ }
+
+ restoredRegistry.onRestoreInstanceState(savedState)
+
+ restoredRegistry.register("key", lifecycleOwner, StartActivityForResult()) {
+ resultReturned = true
+ }
+
+ lifecycleOwner.currentState = Lifecycle.State.STARTED
+
+ assertThat(resultReturned).isTrue()
+ }
+
+ @Test
fun testOnRestoreInstanceState() {
registry.register("key", StartActivityForResult()) {}
diff --git a/activity/activity/src/main/java/androidx/activity/ComponentActivity.java b/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
index 068fe6e..3368985 100644
--- a/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
+++ b/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
@@ -29,6 +29,7 @@
import static androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult.EXTRA_INTENT_SENDER_REQUEST;
import static androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult.EXTRA_SEND_INTENT_EXCEPTION;
+import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
@@ -62,6 +63,7 @@
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat;
+import androidx.core.content.ContextCompat;
import androidx.lifecycle.HasDefaultViewModelProviderFactory;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
@@ -78,6 +80,7 @@
import androidx.savedstate.SavedStateRegistryController;
import androidx.savedstate.SavedStateRegistryOwner;
import androidx.savedstate.ViewTreeSavedStateRegistryOwner;
+import androidx.tracing.Trace;
import java.util.ArrayList;
import java.util.List;
@@ -685,4 +688,27 @@
public final ActivityResultRegistry getActivityResultRegistry() {
return mActivityResultRegistry;
}
+
+ @Override
+ public void reportFullyDrawn() {
+ try {
+ if (Trace.isEnabled()) {
+ Trace.beginSection("reportFullyDrawn() for " + getComponentName());
+ }
+
+ if (Build.VERSION.SDK_INT > 19) {
+ super.reportFullyDrawn();
+ } else if (Build.VERSION.SDK_INT == 19 && ContextCompat.checkSelfPermission(this,
+ Manifest.permission.UPDATE_DEVICE_STATS) == PackageManager.PERMISSION_GRANTED) {
+ // On API 19, the Activity.reportFullyDrawn() method requires the
+ // UPDATE_DEVICE_STATS permission, otherwise it throws an exception. Instead of
+ // throwing, we fall back to a no-op call.
+ super.reportFullyDrawn();
+ }
+ // The Activity.reportFullyDrawn() got added in API 19, fall back to a no-op call if
+ // this method gets called on devices with an earlier version.
+ } finally {
+ Trace.endSection();
+ }
+ }
}
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 d491bf6..36d959f 100644
--- a/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java
+++ b/activity/activity/src/main/java/androidx/activity/result/ActivityResultRegistry.java
@@ -272,7 +272,8 @@
new ArrayList<>(mRcToKey.keySet()));
outState.putStringArrayList(KEY_COMPONENT_ACTIVITY_REGISTERED_KEYS,
new ArrayList<>(mRcToKey.values()));
- outState.putBundle(KEY_COMPONENT_ACTIVITY_PENDING_RESULTS, mPendingResults);
+ outState.putBundle(KEY_COMPONENT_ACTIVITY_PENDING_RESULTS,
+ (Bundle) mPendingResults.clone());
outState.putSerializable(KEY_COMPONENT_ACTIVITY_RANDOM_OBJECT, mRandom);
}
diff --git a/annotation/annotation-experimental-lint/build.gradle b/annotation/annotation-experimental-lint/build.gradle
index 6c42ad7..bc810c7 100644
--- a/annotation/annotation-experimental-lint/build.gradle
+++ b/annotation/annotation-experimental-lint/build.gradle
@@ -51,9 +51,3 @@
description = "Lint checks for the Experimental annotation library. Also enforces the " +
"semantics of Kotlin @Experimental APIs from within Android Java source code."
}
-
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += ["-XXLanguage:-NewInference"]
- }
-}
diff --git a/appcompat/appcompat/api/current.txt b/appcompat/appcompat/api/current.txt
index 5bbbed7..6b91606 100644
--- a/appcompat/appcompat/api/current.txt
+++ b/appcompat/appcompat/api/current.txt
@@ -513,15 +513,14 @@
method public void setTextAppearance(android.content.Context!, int);
}
- public class AppCompatEditText extends android.widget.EditText implements androidx.core.view.TintableBackgroundView {
+ public class AppCompatEditText extends android.widget.EditText implements androidx.core.view.OnReceiveContentViewBehavior androidx.core.view.TintableBackgroundView {
ctor public AppCompatEditText(android.content.Context);
ctor public AppCompatEditText(android.content.Context, android.util.AttributeSet?);
ctor public AppCompatEditText(android.content.Context, android.util.AttributeSet?, int);
- method public androidx.core.widget.RichContentReceiverCompat<android.widget.TextView!>? getRichContentReceiverCompat();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.content.res.ColorStateList? getSupportBackgroundTintList();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.graphics.PorterDuff.Mode? getSupportBackgroundTintMode();
+ method public androidx.core.view.ContentInfoCompat? onReceiveContent(androidx.core.view.ContentInfoCompat);
method public void setBackgroundDrawable(android.graphics.drawable.Drawable?);
- method public void setRichContentReceiverCompat(androidx.core.widget.RichContentReceiverCompat<android.widget.TextView!>?);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportBackgroundTintList(android.content.res.ColorStateList?);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportBackgroundTintMode(android.graphics.PorterDuff.Mode?);
method public void setTextAppearance(android.content.Context!, int);
diff --git a/appcompat/appcompat/api/public_plus_experimental_current.txt b/appcompat/appcompat/api/public_plus_experimental_current.txt
index cb1efa6..8886644 100644
--- a/appcompat/appcompat/api/public_plus_experimental_current.txt
+++ b/appcompat/appcompat/api/public_plus_experimental_current.txt
@@ -513,15 +513,14 @@
method public void setTextAppearance(android.content.Context!, int);
}
- public class AppCompatEditText extends android.widget.EditText implements androidx.core.view.TintableBackgroundView {
+ public class AppCompatEditText extends android.widget.EditText implements androidx.core.view.OnReceiveContentViewBehavior androidx.core.view.TintableBackgroundView {
ctor public AppCompatEditText(android.content.Context);
ctor public AppCompatEditText(android.content.Context, android.util.AttributeSet?);
ctor public AppCompatEditText(android.content.Context, android.util.AttributeSet?, int);
- method public androidx.core.widget.RichContentReceiverCompat<android.widget.TextView!>? getRichContentReceiverCompat();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.content.res.ColorStateList? getSupportBackgroundTintList();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.graphics.PorterDuff.Mode? getSupportBackgroundTintMode();
+ method public androidx.core.view.ContentInfoCompat? onReceiveContent(androidx.core.view.ContentInfoCompat);
method public void setBackgroundDrawable(android.graphics.drawable.Drawable?);
- method public void setRichContentReceiverCompat(androidx.core.widget.RichContentReceiverCompat<android.widget.TextView!>?);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportBackgroundTintList(android.content.res.ColorStateList?);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportBackgroundTintMode(android.graphics.PorterDuff.Mode?);
method public void setTextAppearance(android.content.Context!, int);
diff --git a/appcompat/appcompat/api/restricted_current.txt b/appcompat/appcompat/api/restricted_current.txt
index 14d5f94..7439cfd 100644
--- a/appcompat/appcompat/api/restricted_current.txt
+++ b/appcompat/appcompat/api/restricted_current.txt
@@ -1394,15 +1394,14 @@
method public static void preload();
}
- public class AppCompatEditText extends android.widget.EditText implements androidx.core.view.TintableBackgroundView {
+ public class AppCompatEditText extends android.widget.EditText implements androidx.core.view.OnReceiveContentViewBehavior androidx.core.view.TintableBackgroundView {
ctor public AppCompatEditText(android.content.Context);
ctor public AppCompatEditText(android.content.Context, android.util.AttributeSet?);
ctor public AppCompatEditText(android.content.Context, android.util.AttributeSet?, int);
- method public androidx.core.widget.RichContentReceiverCompat<android.widget.TextView!>? getRichContentReceiverCompat();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.content.res.ColorStateList? getSupportBackgroundTintList();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public android.graphics.PorterDuff.Mode? getSupportBackgroundTintMode();
+ method public androidx.core.view.ContentInfoCompat? onReceiveContent(androidx.core.view.ContentInfoCompat);
method public void setBackgroundDrawable(android.graphics.drawable.Drawable?);
- method public void setRichContentReceiverCompat(androidx.core.widget.RichContentReceiverCompat<android.widget.TextView!>?);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportBackgroundTintList(android.content.res.ColorStateList?);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setSupportBackgroundTintMode(android.graphics.PorterDuff.Mode?);
method public void setTextAppearance(android.content.Context!, int);
diff --git a/appcompat/appcompat/src/androidTest/AndroidManifest.xml b/appcompat/appcompat/src/androidTest/AndroidManifest.xml
index 9af9433..c216f56 100644
--- a/appcompat/appcompat/src/androidTest/AndroidManifest.xml
+++ b/appcompat/appcompat/src/androidTest/AndroidManifest.xml
@@ -100,8 +100,8 @@
android:theme="@style/Theme.TextColors"/>
<activity
- android:name="androidx.appcompat.widget.AppCompatEditTextRichContentReceiverActivity"
- android:label="@string/app_compat_edit_text_rich_content_receiver_activity"
+ android:name="androidx.appcompat.widget.AppCompatEditTextReceiveContentActivity"
+ android:label="@string/app_compat_edit_text_receive_content_activity"
android:theme="@style/Theme.AppCompat.Light"/>
<activity
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextRichContentReceiverActivity.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextReceiveContentActivity.java
similarity index 83%
rename from appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextRichContentReceiverActivity.java
rename to appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextReceiveContentActivity.java
index 9601ac1..4dc2c31 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextRichContentReceiverActivity.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextReceiveContentActivity.java
@@ -18,9 +18,9 @@
import androidx.appcompat.test.R;
import androidx.appcompat.testutils.BaseTestActivity;
-public class AppCompatEditTextRichContentReceiverActivity extends BaseTestActivity {
+public class AppCompatEditTextReceiveContentActivity extends BaseTestActivity {
@Override
protected int getContentViewLayoutResId() {
- return R.layout.appcompat_edittext_richcontentreceiver_activity;
+ return R.layout.appcompat_edittext_receive_content_activity;
}
}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextReceiveContentTest.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextReceiveContentTest.java
new file mode 100644
index 0000000..8f46094
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextReceiveContentTest.java
@@ -0,0 +1,559 @@
+/*
+ * 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.appcompat.widget;
+
+import static androidx.core.view.ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT;
+import static androidx.core.view.ContentInfoCompat.SOURCE_CLIPBOARD;
+import static androidx.core.view.ContentInfoCompat.SOURCE_INPUT_METHOD;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.text.SpannableStringBuilder;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputContentInfo;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.test.R;
+import androidx.core.util.ObjectsCompat;
+import androidx.core.view.ContentInfoCompat;
+import androidx.core.view.OnReceiveContentListener;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.inputmethod.EditorInfoCompat;
+import androidx.core.view.inputmethod.InputConnectionCompat;
+import androidx.core.view.inputmethod.InputContentInfoCompat;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.rule.ActivityTestRule;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mockito;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class AppCompatEditTextReceiveContentTest {
+ private static final String[] MIME_TYPES_IMAGES = new String[] {"image/*"};
+ private static final Uri SAMPLE_CONTENT_URI = Uri.parse("content://com.example/path");
+
+ @Rule
+ public final ActivityTestRule<AppCompatEditTextReceiveContentActivity> mActivityTestRule =
+ new ActivityTestRule<>(AppCompatEditTextReceiveContentActivity.class);
+
+ private Context mContext;
+ private AppCompatEditText mEditText;
+ private OnReceiveContentListener mMockReceiver;
+ private ClipboardManager mClipboardManager;
+
+ @UiThreadTest
+ @Before
+ public void before() {
+ AppCompatActivity activity = mActivityTestRule.getActivity();
+ mContext = activity;
+ mEditText = activity.findViewById(R.id.edit_text);
+
+ mMockReceiver = Mockito.mock(OnReceiveContentListener.class);
+
+ mClipboardManager = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
+
+ // Clear the clipboard
+ if (Build.VERSION.SDK_INT >= 28) {
+ mClipboardManager.clearPrimaryClip();
+ } else {
+ mClipboardManager.setPrimaryClip(ClipData.newPlainText("", ""));
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ public void testOnCreateInputConnection_nullEditorInfo() throws Exception {
+ setTextAndCursor("xz", 1);
+ try {
+ mEditText.onCreateInputConnection(null);
+ Assert.fail("Expected NullPointerException");
+ } catch (NullPointerException expected) {
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ public void testOnCreateInputConnection_noReceiver() throws Exception {
+ setTextAndCursor("xz", 1);
+
+ // Call onCreateInputConnection() and assert that contentMimeTypes is not set.
+ EditorInfo editorInfo = new EditorInfo();
+ InputConnection ic = mEditText.onCreateInputConnection(editorInfo);
+ assertThat(ic).isNotNull();
+ assertThat(EditorInfoCompat.getContentMimeTypes(editorInfo)).isEqualTo(new String[0]);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testOnCreateInputConnection_withReceiver() throws Exception {
+ setTextAndCursor("xz", 1);
+
+ // Setup: Configure the receiver to a custom impl.
+ String[] mimeTypes = new String[] {"image/*", "video/mp4"};
+ ViewCompat.setOnReceiveContentListener(mEditText, mimeTypes, mMockReceiver);
+
+ // Call onCreateInputConnection() and assert that contentMimeTypes uses the receiver's MIME
+ // types.
+ EditorInfo editorInfo = new EditorInfo();
+ InputConnection ic = mEditText.onCreateInputConnection(editorInfo);
+ assertThat(ic).isNotNull();
+ verifyZeroInteractions(mMockReceiver);
+ assertThat(EditorInfoCompat.getContentMimeTypes(editorInfo)).isEqualTo(mimeTypes);
+ }
+
+ // ============================================================================================
+ // Tests to verify that the listener is invoked for all the appropriate user interactions:
+ // * Paste from clipboard ("Paste" and "Paste as plain text" actions)
+ // * Content insertion from IME
+ // ============================================================================================
+
+ @UiThreadTest
+ @Test
+ public void testPaste_noReceiver() throws Exception {
+ setTextAndCursor("xz", 1);
+
+ // Setup: Copy text to the clipboard.
+ ClipData clip = ClipData.newPlainText("test", "y");
+ clip = copyToClipboard(clip);
+
+ // Trigger the "Paste" action. This should execute the platform paste handling, so the
+ // content should be inserted according to whatever behavior is implemented in the OS
+ // version that's running.
+ boolean result = triggerContextMenuAction(android.R.id.paste);
+ assertThat(result).isTrue();
+ if (Build.VERSION.SDK_INT <= 20) {
+ // The platform code on Android K and earlier had logic to insert a space before and
+ // after the pasted content (if no space was already present). See
+ // https://cs.android.com/android/platform/superproject/+/android-4.4.4_r2:frameworks/base/core/java/android/widget/TextView.java;l=8526,8527,8528,8545,8546
+ assertTextAndCursorPosition("x y z", 3);
+ } else {
+ assertTextAndCursorPosition("xyz", 2);
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ public void testPaste_withReceiver() throws Exception {
+ setTextAndCursor("xz", 1);
+
+ // Setup: Copy text to the clipboard.
+ ClipData clip = ClipData.newPlainText("test", "y");
+ clip = copyToClipboard(clip);
+
+ // Setup: Configure to use the mock receiver.
+ ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+ // Trigger the "Paste" action and assert that the custom receiver was executed.
+ triggerContextMenuAction(android.R.id.paste);
+ verify(mMockReceiver, times(1)).onReceiveContent(
+ eq(mEditText), payloadEq(clip, SOURCE_CLIPBOARD, 0));
+ verifyNoMoreInteractions(mMockReceiver);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testPaste_withReceiver_resultBoolean() throws Exception {
+ setTextAndCursor("xz", 1);
+
+ // Setup: Copy text to the clipboard.
+ ClipData clip = ClipData.newPlainText("test", "y");
+ clip = copyToClipboard(clip);
+
+ // Setup: Configure to use the mock receiver.
+ ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+ // Trigger the "Paste" action and assert the boolean it returns.
+ ContentInfoCompat toReturn = new ContentInfoCompat.Builder(clip, SOURCE_CLIPBOARD).build();
+ when(mMockReceiver.onReceiveContent(eq(mEditText), any(ContentInfoCompat.class)))
+ .thenReturn(toReturn);
+ boolean result = triggerContextMenuAction(android.R.id.paste);
+ assertThat(result).isTrue();
+
+ when(mMockReceiver.onReceiveContent(eq(mEditText), any(ContentInfoCompat.class)))
+ .thenReturn(null);
+ result = triggerContextMenuAction(android.R.id.paste);
+ assertThat(result).isTrue();
+ }
+
+ @UiThreadTest
+ @Test
+ public void testPaste_withReceiver_unsupportedMimeType() throws Exception {
+ setTextAndCursor("xz", 1);
+
+ // Setup: Copy a URI to the clipboard with a MIME type that's not supported by the receiver.
+ ClipData clip = new ClipData("test", new String[]{"video/mp4"},
+ new ClipData.Item("text", null, Uri.parse("content://com.example/path")));
+ clip = copyToClipboard(clip);
+
+ // Setup: Configure to use the mock receiver.
+ String[] mimeTypes = new String[] {"image/*"};
+ ViewCompat.setOnReceiveContentListener(mEditText, mimeTypes, mMockReceiver);
+
+ // Trigger the "Paste" action and assert that the custom receiver was executed. This
+ // confirms that the receiver is invoked (give a chance to handle the content via some
+ // fallback) even if the MIME type of the content is not one of the receiver's supported
+ // MIME types.
+ triggerContextMenuAction(android.R.id.paste);
+ verify(mMockReceiver, times(1)).onReceiveContent(
+ eq(mEditText), payloadEq(clip, SOURCE_CLIPBOARD, 0));
+ verifyNoMoreInteractions(mMockReceiver);
+ }
+
+ @SdkSuppress(minSdkVersion = 23) // The action "Paste as plain text" was added in SDK 23.
+ @UiThreadTest
+ @Test
+ public void testPasteAsPlainText_noReceiver() throws Exception {
+ setTextAndCursor("xz", 1);
+
+ // Setup: Copy HTML to the clipboard.
+ ClipData clip = ClipData.newHtmlText("test", "*y*", "<b>y</b>");
+ clip = copyToClipboard(clip);
+
+ // Trigger the "Paste as plain text" action. This should execute the platform paste
+ // handling, so the content should be inserted according to whatever behavior is implemented
+ // in the OS version that's running.
+ boolean result = triggerContextMenuAction(android.R.id.pasteAsPlainText);
+ assertThat(result).isTrue();
+ assertTextAndCursorPosition("x*y*z", 4);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testPasteAsPlainText_withReceiver() throws Exception {
+ setTextAndCursor("xz", 1);
+
+ // Setup: Copy text to the clipboard.
+ ClipData clip = ClipData.newPlainText("test", "y");
+ clip = copyToClipboard(clip);
+
+ // Setup: Configure to use the mock receiver.
+ ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+ // Trigger the "Paste as plain text" action and assert that the custom receiver was
+ // executed.
+ triggerContextMenuAction(android.R.id.pasteAsPlainText);
+ verify(mMockReceiver, times(1)).onReceiveContent(
+ eq(mEditText), payloadEq(clip, SOURCE_CLIPBOARD, FLAG_CONVERT_TO_PLAIN_TEXT));
+ verifyNoMoreInteractions(mMockReceiver);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testPasteAsPlainText_withReceiver_unsupportedMimeType() throws Exception {
+ setTextAndCursor("xz", 1);
+
+ // Setup: Copy a URI to the clipboard with a MIME type that's not supported by the receiver.
+ ClipData clip = new ClipData("test", new String[]{"video/mp4"},
+ new ClipData.Item("text", null, Uri.parse("content://com.example/path")));
+ clip = copyToClipboard(clip);
+
+ // Setup: Configure to use the mock receiver.
+ ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+ // Trigger the "Paste as plain text" action and assert that the custom receiver was
+ // executed. This confirms that the receiver is invoked (given a chance to handle the
+ // content via some fallback) even if the MIME type of the content is not one of the
+ // receiver's supported MIME types.
+ triggerContextMenuAction(android.R.id.pasteAsPlainText);
+ verify(mMockReceiver, times(1)).onReceiveContent(
+ eq(mEditText), payloadEq(clip, SOURCE_CLIPBOARD, FLAG_CONVERT_TO_PLAIN_TEXT));
+ verifyNoMoreInteractions(mMockReceiver);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testImeCommitContent_noReceiver() throws Exception {
+ setTextAndCursor("xz", 1);
+
+ // Trigger the IME's commitContent() call and assert its outcome.
+ boolean result = triggerImeCommitContentViaCompat("image/png");
+ assertThat(result).isFalse();
+ assertTextAndCursorPosition("xz", 1);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testImeCommitContent_withReceiver() throws Exception {
+ setTextAndCursor("xz", 1);
+
+ // Setup: Configure the receiver to a custom impl.
+ ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+ // Trigger the IME's commitContent() call and assert that the custom receiver was executed.
+ triggerImeCommitContentViaCompat("image/png");
+ ClipData clip = ClipData.newRawUri("", SAMPLE_CONTENT_URI);
+ verify(mMockReceiver, times(1)).onReceiveContent(
+ eq(mEditText), payloadEq(clip, SOURCE_INPUT_METHOD, 0));
+ verifyNoMoreInteractions(mMockReceiver);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testImeCommitContent_withReceiver_resultBoolean() throws Exception {
+ setTextAndCursor("xz", 1);
+
+ // Setup: Configure the receiver to a custom impl.
+ ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+ // Trigger the IME's commitContent() call and assert the boolean value it returns.
+ when(mMockReceiver.onReceiveContent(eq(mEditText), any(ContentInfoCompat.class)))
+ .thenReturn(null);
+ boolean result1 = triggerImeCommitContentViaCompat("image/png");
+ ClipData clip = ClipData.newRawUri("", SAMPLE_CONTENT_URI);
+ ContentInfoCompat payloadToReturn =
+ new ContentInfoCompat.Builder(clip, SOURCE_INPUT_METHOD).build();
+ when(mMockReceiver.onReceiveContent(eq(mEditText), any(ContentInfoCompat.class)))
+ .thenReturn(payloadToReturn);
+ boolean result2 = triggerImeCommitContentViaCompat("image/png");
+ verify(mMockReceiver, times(2)).onReceiveContent(
+ eq(mEditText), payloadEq(clip, SOURCE_INPUT_METHOD, 0));
+ if (Build.VERSION.SDK_INT >= 25) {
+ // On SDK >= 25, the boolean result depends on the return value from the receiver.
+ assertThat(result1).isTrue();
+ assertThat(result2).isFalse();
+ } else {
+ // On SDK <= 24, commitContent() is handled via InputConnection.performPrivateCommand().
+ // This ends up returning true whenever the command is sent, regardless of the return
+ // value of the underlying operation.
+ // Relevant code links:
+ // https://osscs.corp.google.com/androidx/platform/frameworks/support/+/androidx-master-dev:core/core/src/main/java/androidx/core/view/inputmethod/InputConnectionCompat.java;l=294;drc=0c365e84832f5ec5e393be28ab1c618eb18bab1e
+ // https://cs.android.com/android/platform/superproject/+/android-7.0.0_r6:frameworks/base/core/java/com/android/internal/widget/EditableInputConnection.java;l=168
+ assertThat(result1).isTrue();
+ assertThat(result2).isTrue();
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ public void testImeCommitContent_withReceiver_unsupportedMimeType() throws Exception {
+ setTextAndCursor("xz", 1);
+
+ // Setup: Configure the receiver to a custom impl.
+ ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+ // Trigger the IME's commitContent() call and assert that the custom receiver was not
+ // executed. This is because InputConnectionCompat.commitContent() checks the supported MIME
+ // types before proceeding.
+ triggerImeCommitContentViaCompat("video/mp4");
+ verifyZeroInteractions(mMockReceiver);
+ }
+
+ @SdkSuppress(minSdkVersion = 25) // InputConnection.commitContent() was added in SDK 25.
+ @UiThreadTest
+ @Test
+ public void testImeCommitContent_direct_withReceiver_unsupportedMimeType() throws Exception {
+ setTextAndCursor("xz", 1);
+
+ // Setup: Configure the receiver to a custom impl.
+ ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+ // Trigger the IME's commitContent() call and assert that the custom receiver was executed.
+ triggerImeCommitContentDirect("video/mp4");
+ ClipData clip = ClipData.newRawUri("", SAMPLE_CONTENT_URI);
+ verify(mMockReceiver, times(1)).onReceiveContent(
+ eq(mEditText), payloadEq(clip, SOURCE_INPUT_METHOD, 0));
+ verifyNoMoreInteractions(mMockReceiver);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testImeCommitContent_linkUri() throws Exception {
+ setTextAndCursor("xz", 1);
+
+ // Setup: Configure the receiver to a mock impl.
+ ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+ // Trigger the IME's commitContent() call with a linkUri and assert receiver extras.
+ Uri sampleLinkUri = Uri.parse("http://example.com");
+ triggerImeCommitContentViaCompat("image/png", sampleLinkUri, null);
+ ClipData clip = ClipData.newRawUri("expected", SAMPLE_CONTENT_URI);
+ verify(mMockReceiver, times(1)).onReceiveContent(
+ eq(mEditText),
+ payloadEq(clip, SOURCE_INPUT_METHOD, 0, sampleLinkUri, null));
+ }
+
+ @UiThreadTest
+ @Test
+ public void testImeCommitContent_opts() throws Exception {
+ setTextAndCursor("xz", 1);
+
+ // Setup: Configure the receiver to a mock impl.
+ ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+ // Trigger the IME's commitContent() call with opts and assert receiver extras.
+ String sampleOptValue = "sampleOptValue";
+ triggerImeCommitContentViaCompat("image/png", null, sampleOptValue);
+ ClipData clip = ClipData.newRawUri("expected", SAMPLE_CONTENT_URI);
+ verify(mMockReceiver, times(1)).onReceiveContent(
+ eq(mEditText),
+ payloadEq(clip, SOURCE_INPUT_METHOD, 0, null, sampleOptValue));
+ }
+
+ @UiThreadTest
+ @Test
+ public void testImeCommitContent_linkUriAndOpts() throws Exception {
+ setTextAndCursor("xz", 1);
+
+ // Setup: Configure the receiver to a mock impl.
+ ViewCompat.setOnReceiveContentListener(mEditText, MIME_TYPES_IMAGES, mMockReceiver);
+
+ // Trigger the IME's commitContent() call with a linkUri & opts and assert receiver extras.
+ Uri sampleLinkUri = Uri.parse("http://example.com");
+ String sampleOptValue = "sampleOptValue";
+ triggerImeCommitContentViaCompat("image/png", sampleLinkUri, sampleOptValue);
+ ClipData clip = ClipData.newRawUri("expected", SAMPLE_CONTENT_URI);
+ verify(mMockReceiver, times(1)).onReceiveContent(
+ eq(mEditText),
+ payloadEq(clip, SOURCE_INPUT_METHOD, 0, sampleLinkUri, sampleOptValue));
+ }
+
+ private boolean triggerContextMenuAction(final int actionId) {
+ return mEditText.onTextContextMenuItem(actionId);
+ }
+
+ private boolean triggerImeCommitContentViaCompat(String mimeType) {
+ return triggerImeCommitContentViaCompat(mimeType, null, null);
+ }
+
+ private boolean triggerImeCommitContentViaCompat(String mimeType, Uri linkUri, String extra) {
+ final InputContentInfoCompat contentInfo = new InputContentInfoCompat(
+ SAMPLE_CONTENT_URI,
+ new ClipDescription("from test", new String[]{mimeType}),
+ linkUri);
+ final Bundle opts;
+ if (extra == null) {
+ opts = null;
+ } else {
+ opts = new Bundle();
+ opts.putString(PayloadArgumentMatcher.EXTRA_KEY, extra);
+ }
+ EditorInfo editorInfo = new EditorInfo();
+ InputConnection ic = mEditText.onCreateInputConnection(editorInfo);
+ return InputConnectionCompat.commitContent(ic, editorInfo, contentInfo, 0, opts);
+ }
+
+ private boolean triggerImeCommitContentDirect(String mimeType) {
+ final InputContentInfo contentInfo = new InputContentInfo(
+ SAMPLE_CONTENT_URI,
+ new ClipDescription("from test", new String[]{mimeType}),
+ null);
+ EditorInfo editorInfo = new EditorInfo();
+ InputConnection ic = mEditText.onCreateInputConnection(editorInfo);
+ return ic.commitContent(contentInfo, 0, null);
+ }
+
+ private void setTextAndCursor(final String text, final int cursorPosition) {
+ mEditText.requestFocus();
+ SpannableStringBuilder ssb = new SpannableStringBuilder(text);
+ mEditText.setText(ssb);
+ mEditText.setSelection(cursorPosition);
+ assertThat(mEditText.hasFocus()).isTrue();
+ assertTextAndCursorPosition(text, cursorPosition);
+ }
+
+ private void assertTextAndCursorPosition(String expectedText, int cursorPosition) {
+ assertThat(mEditText.getText().toString()).isEqualTo(expectedText);
+ assertThat(mEditText.getSelectionStart()).isEqualTo(cursorPosition);
+ assertThat(mEditText.getSelectionEnd()).isEqualTo(cursorPosition);
+ }
+
+ private ClipData copyToClipboard(final ClipData clip) {
+ mClipboardManager.setPrimaryClip(clip);
+ ClipData primaryClip = mClipboardManager.getPrimaryClip();
+ assertThat(primaryClip).isNotNull();
+ return primaryClip;
+ }
+
+ private static ContentInfoCompat payloadEq(@NonNull ClipData clip, int source, int flags) {
+ return argThat(new PayloadArgumentMatcher(clip, source, flags, null, null));
+ }
+
+ private static ContentInfoCompat payloadEq(@NonNull ClipData clip, int source, int flags,
+ @Nullable Uri linkUri, @Nullable String extra) {
+ return argThat(new PayloadArgumentMatcher(clip, source, flags, linkUri, extra));
+ }
+
+ private static class PayloadArgumentMatcher implements ArgumentMatcher<ContentInfoCompat> {
+ public static final String EXTRA_KEY = "testExtra";
+
+ @NonNull
+ private final ClipData mClip;
+ private final int mSource;
+ private final int mFlags;
+ @Nullable
+ private final Uri mLinkUri;
+ @Nullable
+ private final String mExtra;
+
+ private PayloadArgumentMatcher(@NonNull ClipData clip, int source, int flags,
+ @Nullable Uri linkUri, @Nullable String extra) {
+ mClip = clip;
+ mSource = source;
+ mFlags = flags;
+ mLinkUri = linkUri;
+ mExtra = extra;
+ }
+
+ @Override
+ public boolean matches(ContentInfoCompat actual) {
+ ClipData.Item expectedItem = mClip.getItemAt(0);
+ ClipData.Item actualItem = actual.getClip().getItemAt(0);
+ return ObjectsCompat.equals(expectedItem.getText(), actualItem.getText())
+ && ObjectsCompat.equals(expectedItem.getUri(), actualItem.getUri())
+ && mSource == actual.getSource()
+ && mFlags == actual.getFlags()
+ && ObjectsCompat.equals(mLinkUri, actual.getLinkUri())
+ && extrasMatch(actual.getExtras());
+ }
+
+ private boolean extrasMatch(Bundle actualExtras) {
+ if (mExtra == null) {
+ return actualExtras == null;
+ }
+ String actualExtraValue = actualExtras.getString(EXTRA_KEY);
+ return ObjectsCompat.equals(mExtra, actualExtraValue);
+ }
+ }
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextRichContentReceiverTest.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextRichContentReceiverTest.java
deleted file mode 100644
index 5f68405..0000000
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextRichContentReceiverTest.java
+++ /dev/null
@@ -1,489 +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.appcompat.widget;
-
-import static androidx.core.widget.RichContentReceiverCompat.FLAG_CONVERT_TO_PLAIN_TEXT;
-import static androidx.core.widget.RichContentReceiverCompat.SOURCE_CLIPBOARD;
-import static androidx.core.widget.RichContentReceiverCompat.SOURCE_INPUT_METHOD;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
-
-import android.content.ClipData;
-import android.content.ClipDescription;
-import android.content.ClipboardManager;
-import android.content.Context;
-import android.net.Uri;
-import android.os.Build;
-import android.text.SpannableStringBuilder;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
-import android.view.inputmethod.InputContentInfo;
-import android.widget.TextView;
-
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.test.R;
-import androidx.core.view.inputmethod.EditorInfoCompat;
-import androidx.core.view.inputmethod.InputConnectionCompat;
-import androidx.core.view.inputmethod.InputContentInfoCompat;
-import androidx.core.widget.RichContentReceiverCompat;
-import androidx.core.widget.TextViewRichContentReceiverCompat;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.MediumTest;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.rule.ActivityTestRule;
-
-import com.google.common.collect.ImmutableSet;
-
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentMatcher;
-import org.mockito.Mockito;
-
-import java.util.Set;
-
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class AppCompatEditTextRichContentReceiverTest {
- private static final Set<String> ALL_TEXT_AND_IMAGE_MIME_TYPES = ImmutableSet.of(
- "text/*", "image/*");
-
- @Rule
- public final ActivityTestRule<AppCompatEditTextRichContentReceiverActivity> mActivityTestRule =
- new ActivityTestRule<>(AppCompatEditTextRichContentReceiverActivity.class);
-
- private Context mContext;
- private AppCompatEditText mEditText;
- private RichContentReceiverCompat<TextView> mMockReceiver;
- private ClipboardManager mClipboardManager;
-
- @UiThreadTest
- @Before
- public void before() {
- AppCompatActivity activity = mActivityTestRule.getActivity();
- mContext = activity;
- mEditText = activity.findViewById(R.id.edit_text_default_values);
-
- mMockReceiver = Mockito.mock(RichContentReceiverCompat.class);
-
- mClipboardManager = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
-
- // Clear the clipboard
- if (Build.VERSION.SDK_INT >= 28) {
- mClipboardManager.clearPrimaryClip();
- } else {
- mClipboardManager.setPrimaryClip(ClipData.newPlainText("", ""));
- }
- }
-
- // ============================================================================================
- // Tests to verify APIs/accessors/defaults related to RichContentReceiver.
- // ============================================================================================
-
- @UiThreadTest
- @Test
- public void testGetAndSetRichContentReceiverCompat() throws Exception {
- // Verify that by default the getter returns null.
- assertThat(mEditText.getRichContentReceiverCompat()).isNull();
-
- // Verify that after setting a custom receiver, the getter returns it.
- TextViewRichContentReceiverCompat receiver = new TextViewRichContentReceiverCompat() {};
- mEditText.setRichContentReceiverCompat(receiver);
- assertThat(mEditText.getRichContentReceiverCompat()).isSameInstanceAs(receiver);
-
- // Verify that the receiver can be reset by passing null.
- mEditText.setRichContentReceiverCompat(null);
- assertThat(mEditText.getRichContentReceiverCompat()).isNull();
- }
-
- @UiThreadTest
- @Test
- public void testOnCreateInputConnection_nullEditorInfo() throws Exception {
- setTextAndCursor("xz", 1);
- try {
- mEditText.onCreateInputConnection(null);
- Assert.fail("Expected NullPointerException");
- } catch (NullPointerException expected) {
- }
- }
-
- @UiThreadTest
- @Test
- public void testOnCreateInputConnection_noReceiver() throws Exception {
- setTextAndCursor("xz", 1);
-
- // Call onCreateInputConnection() and assert that contentMimeTypes is not set.
- EditorInfo editorInfo = new EditorInfo();
- InputConnection ic = mEditText.onCreateInputConnection(editorInfo);
- assertThat(ic).isNotNull();
- assertThat(EditorInfoCompat.getContentMimeTypes(editorInfo)).isEqualTo(new String[0]);
- }
-
- @UiThreadTest
- @Test
- public void testOnCreateInputConnection_withReceiver() throws Exception {
- setTextAndCursor("xz", 1);
-
- // Setup: Configure the receiver to a custom impl.
- Set<String> receiverMimeTypes = ImmutableSet.of("text/plain", "image/png", "video/mp4");
- when(mMockReceiver.getSupportedMimeTypes()).thenReturn(receiverMimeTypes);
- mEditText.setRichContentReceiverCompat(mMockReceiver);
-
- // Call onCreateInputConnection() and assert that contentMimeTypes is set from the receiver.
- EditorInfo editorInfo = new EditorInfo();
- InputConnection ic = mEditText.onCreateInputConnection(editorInfo);
- assertThat(ic).isNotNull();
- verify(mMockReceiver, times(1)).getSupportedMimeTypes();
- verifyNoMoreInteractions(mMockReceiver);
- assertThat(EditorInfoCompat.getContentMimeTypes(editorInfo))
- .isEqualTo(receiverMimeTypes.toArray(new String[0]));
- }
-
- // ============================================================================================
- // Tests to verify that the receiver callback is invoked for all the appropriate user
- // interactions:
- // * Paste from clipboard ("Paste" and "Paste as plain text" actions)
- // * Content insertion from IME
- // ============================================================================================
-
- @UiThreadTest
- @Test
- public void testPaste_noReceiver() throws Exception {
- setTextAndCursor("xz", 1);
-
- // Setup: Copy text to the clipboard.
- ClipData clip = ClipData.newPlainText("test", "y");
- clip = copyToClipboard(clip);
-
- // Trigger the "Paste" action. This should execute the platform paste handling, so the
- // content should be inserted according to whatever behavior is implemented in the OS
- // version that's running.
- boolean result = triggerContextMenuAction(android.R.id.paste);
- assertThat(result).isTrue();
- if (Build.VERSION.SDK_INT <= 20) {
- // The platform code on Android K and earlier had logic to insert a space before and
- // after the pasted content (if no space was already present). See
- // https://cs.android.com/android/platform/superproject/+/android-4.4.4_r2:frameworks/base/core/java/android/widget/TextView.java;l=8526,8527,8528,8545,8546
- assertTextAndCursorPosition("x y z", 3);
- } else {
- assertTextAndCursorPosition("xyz", 2);
- }
- }
-
- @UiThreadTest
- @Test
- public void testPaste_withReceiver() throws Exception {
- setTextAndCursor("xz", 1);
-
- // Setup: Copy text to the clipboard.
- ClipData clip = ClipData.newPlainText("test", "y");
- clip = copyToClipboard(clip);
-
- // Setup: Configure to use the mock receiver.
- mEditText.setRichContentReceiverCompat(mMockReceiver);
-
- // Trigger the "Paste" action and assert that the custom receiver was executed.
- triggerContextMenuAction(android.R.id.paste);
- verify(mMockReceiver, times(1)).onReceive(
- eq(mEditText), clipEq(clip), eq(SOURCE_CLIPBOARD), eq(0));
- verifyNoMoreInteractions(mMockReceiver);
- }
-
- @UiThreadTest
- @Test
- public void testPaste_withReceiver_resultBoolean() throws Exception {
- setTextAndCursor("xz", 1);
-
- // Setup: Copy text to the clipboard.
- ClipData clip = ClipData.newPlainText("test", "y");
- clip = copyToClipboard(clip);
-
- // Setup: Configure to use the mock receiver.
- mEditText.setRichContentReceiverCompat(mMockReceiver);
-
- // Trigger the "Paste" action and assert that the boolean result is true regardless of
- // the receiver's return value.
- when(mMockReceiver.onReceive(eq(mEditText), eq(clip), eq(SOURCE_CLIPBOARD),
- eq(FLAG_CONVERT_TO_PLAIN_TEXT))).thenReturn(true);
- boolean result = triggerContextMenuAction(android.R.id.paste);
- assertThat(result).isTrue();
-
- when(mMockReceiver.onReceive(eq(mEditText), eq(clip), eq(SOURCE_CLIPBOARD),
- eq(FLAG_CONVERT_TO_PLAIN_TEXT))).thenReturn(false);
- result = triggerContextMenuAction(android.R.id.paste);
- assertThat(result).isTrue();
- }
-
- @UiThreadTest
- @Test
- public void testPaste_withReceiver_unsupportedMimeType() throws Exception {
- setTextAndCursor("xz", 1);
-
- // Setup: Copy a URI to the clipboard with a MIME type that's not supported by the receiver.
- ClipData clip = new ClipData("test", new String[]{"video/mp4"},
- new ClipData.Item("text", null, Uri.parse("content://com.example/path")));
- clip = copyToClipboard(clip);
-
- // Setup: Configure to use the mock receiver.
- mEditText.setRichContentReceiverCompat(mMockReceiver);
-
- // Trigger the "Paste" action and assert that the custom receiver was executed. This
- // confirms that the receiver is invoked (give a chance to handle the content via some
- // fallback) even if the MIME type of the content is not one of the receiver's supported
- // MIME types.
- triggerContextMenuAction(android.R.id.paste);
- verify(mMockReceiver, times(1)).onReceive(
- eq(mEditText), clipEq(clip), eq(SOURCE_CLIPBOARD), eq(0));
- verifyNoMoreInteractions(mMockReceiver);
- }
-
- @SdkSuppress(minSdkVersion = 23) // The action "Paste as plain text" was added in SDK 23.
- @UiThreadTest
- @Test
- public void testPasteAsPlainText_noReceiver() throws Exception {
- setTextAndCursor("xz", 1);
-
- // Setup: Copy HTML to the clipboard.
- ClipData clip = ClipData.newHtmlText("test", "*y*", "<b>y</b>");
- clip = copyToClipboard(clip);
-
- // Trigger the "Paste as plain text" action. This should execute the platform paste
- // handling, so the content should be inserted according to whatever behavior is implemented
- // in the OS version that's running.
- boolean result = triggerContextMenuAction(android.R.id.pasteAsPlainText);
- assertThat(result).isTrue();
- assertTextAndCursorPosition("x*y*z", 4);
- }
-
- @UiThreadTest
- @Test
- public void testPasteAsPlainText_withReceiver() throws Exception {
- setTextAndCursor("xz", 1);
-
- // Setup: Copy text to the clipboard.
- ClipData clip = ClipData.newPlainText("test", "y");
- clip = copyToClipboard(clip);
-
- // Setup: Configure to use the mock receiver.
- mEditText.setRichContentReceiverCompat(mMockReceiver);
-
- // Trigger the "Paste as plain text" action and assert that the custom receiver was
- // executed.
- triggerContextMenuAction(android.R.id.pasteAsPlainText);
- verify(mMockReceiver, times(1)).onReceive(
- eq(mEditText), clipEq(clip),
- eq(SOURCE_CLIPBOARD), eq(FLAG_CONVERT_TO_PLAIN_TEXT));
- verifyNoMoreInteractions(mMockReceiver);
- }
-
- @UiThreadTest
- @Test
- public void testPasteAsPlainText_withReceiver_unsupportedMimeType() throws Exception {
- setTextAndCursor("xz", 1);
-
- // Setup: Copy a URI to the clipboard with a MIME type that's not supported by the receiver.
- ClipData clip = new ClipData("test", new String[]{"video/mp4"},
- new ClipData.Item("text", null, Uri.parse("content://com.example/path")));
- clip = copyToClipboard(clip);
-
- // Setup: Configure to use the mock receiver.
- mEditText.setRichContentReceiverCompat(mMockReceiver);
-
- // Trigger the "Paste as plain text" action and assert that the custom receiver was
- // executed. This confirms that the receiver is invoked (given a chance to handle the
- // content via some fallback) even if the MIME type of the content is not one of the
- // receiver's supported MIME types.
- triggerContextMenuAction(android.R.id.pasteAsPlainText);
- verify(mMockReceiver, times(1)).onReceive(
- eq(mEditText), clipEq(clip),
- eq(SOURCE_CLIPBOARD), eq(FLAG_CONVERT_TO_PLAIN_TEXT));
- verifyNoMoreInteractions(mMockReceiver);
- }
-
- @UiThreadTest
- @Test
- public void testImeCommitContent_noReceiver() throws Exception {
- setTextAndCursor("xz", 1);
-
- // Trigger the IME's commitContent() call and assert its outcome.
- boolean result = triggerImeCommitContentViaCompat("image/png");
- assertThat(result).isFalse();
- assertTextAndCursorPosition("xz", 1);
- }
-
- @UiThreadTest
- @Test
- public void testImeCommitContent_withReceiver() throws Exception {
- setTextAndCursor("xz", 1);
-
- // Setup: Configure the receiver to a custom impl that supports all text and images.
- when(mMockReceiver.getSupportedMimeTypes()).thenReturn(ALL_TEXT_AND_IMAGE_MIME_TYPES);
- mEditText.setRichContentReceiverCompat(mMockReceiver);
-
- // Trigger the IME's commitContent() call and assert that the custom receiver was executed.
- triggerImeCommitContentViaCompat("image/png");
- verify(mMockReceiver, times(1)).getSupportedMimeTypes();
- verify(mMockReceiver, times(1)).onReceive(
- eq(mEditText), any(ClipData.class), eq(SOURCE_INPUT_METHOD), eq(0));
- verifyNoMoreInteractions(mMockReceiver);
- }
-
- @UiThreadTest
- @Test
- public void testImeCommitContent_withReceiver_resultBoolean() throws Exception {
- setTextAndCursor("xz", 1);
-
- // Setup: Configure the receiver to a custom impl that supports all text and images.
- when(mMockReceiver.getSupportedMimeTypes()).thenReturn(ALL_TEXT_AND_IMAGE_MIME_TYPES);
- mEditText.setRichContentReceiverCompat(mMockReceiver);
-
- // Trigger the IME's commitContent() call, once when the mock receiver is configured to
- // return true and once when the mock receiver is configured to return false.
- when(mMockReceiver.onReceive(eq(mEditText), any(ClipData.class), eq(SOURCE_INPUT_METHOD),
- eq(0))).thenReturn(true);
- boolean result1 = triggerImeCommitContentViaCompat("image/png");
- when(mMockReceiver.onReceive(eq(mEditText), any(ClipData.class), eq(SOURCE_INPUT_METHOD),
- eq(0))).thenReturn(false);
- boolean result2 = triggerImeCommitContentViaCompat("image/png");
- verify(mMockReceiver, times(2)).onReceive(
- eq(mEditText), any(ClipData.class), eq(SOURCE_INPUT_METHOD), eq(0));
- if (Build.VERSION.SDK_INT >= 25) {
- // On SDK 25 and above, the boolean result should match the return value from the
- // receiver.
- assertThat(result1).isTrue();
- assertThat(result2).isFalse();
- } else {
- // On SDK 24 and below, commitContent() is handled via
- // InputConnection.performPrivateCommand(). This ends up returning true whenever the
- // command is sent, regardless of the return value of the underlying operation.
- // Relevant code links:
- // https://osscs.corp.google.com/androidx/platform/frameworks/support/+/androidx-master-dev:core/core/src/main/java/androidx/core/view/inputmethod/InputConnectionCompat.java;l=294;drc=0c365e84832f5ec5e393be28ab1c618eb18bab1e
- // https://cs.android.com/android/platform/superproject/+/android-7.0.0_r6:frameworks/base/core/java/com/android/internal/widget/EditableInputConnection.java;l=168
- assertThat(result1).isTrue();
- assertThat(result2).isTrue();
- }
- }
-
- @UiThreadTest
- @Test
- public void testImeCommitContent_withReceiver_unsupportedMimeType() throws Exception {
- setTextAndCursor("xz", 1);
-
- // Setup: Configure the receiver to a custom impl that supports all text and images.
- when(mMockReceiver.getSupportedMimeTypes()).thenReturn(ALL_TEXT_AND_IMAGE_MIME_TYPES);
- mEditText.setRichContentReceiverCompat(mMockReceiver);
-
- // Trigger the IME's commitContent() call and assert that the custom receiver was not
- // executed. This is because InputConnectionCompat.commitContent() checks the supported MIME
- // types before proceeding.
- triggerImeCommitContentViaCompat("video/mp4");
- verify(mMockReceiver, times(1)).getSupportedMimeTypes();
- verifyNoMoreInteractions(mMockReceiver);
- }
-
- @SdkSuppress(minSdkVersion = 25) // InputConnection.commitContent() was added in SDK 25.
- @UiThreadTest
- @Test
- public void testImeCommitContent_direct_withReceiver_unsupportedMimeType() throws Exception {
- setTextAndCursor("xz", 1);
-
- // Setup: Configure the receiver to a custom impl that supports all text and images.
- when(mMockReceiver.getSupportedMimeTypes()).thenReturn(ALL_TEXT_AND_IMAGE_MIME_TYPES);
- mEditText.setRichContentReceiverCompat(mMockReceiver);
-
- // Trigger the IME's commitContent() call and assert that the custom receiver was executed.
- triggerImeCommitContentDirect("video/mp4");
- verify(mMockReceiver, times(1)).getSupportedMimeTypes();
- verify(mMockReceiver, times(1)).onReceive(
- eq(mEditText), any(ClipData.class), eq(SOURCE_INPUT_METHOD), eq(0));
- verifyNoMoreInteractions(mMockReceiver);
- }
-
- private boolean triggerContextMenuAction(final int actionId) {
- return mEditText.onTextContextMenuItem(actionId);
- }
-
- private boolean triggerImeCommitContentViaCompat(String mimeType) {
- final InputContentInfoCompat contentInfo = new InputContentInfoCompat(
- Uri.parse("content://com.example/path"),
- new ClipDescription("from test", new String[]{mimeType}),
- Uri.parse("https://example.com"));
- EditorInfo editorInfo = new EditorInfo();
- InputConnection ic = mEditText.onCreateInputConnection(editorInfo);
- return InputConnectionCompat.commitContent(ic, editorInfo, contentInfo, 0, null);
- }
-
- private boolean triggerImeCommitContentDirect(String mimeType) {
- final InputContentInfo contentInfo = new InputContentInfo(
- Uri.parse("content://com.example/path"),
- new ClipDescription("from test", new String[]{mimeType}),
- Uri.parse("https://example.com"));
- EditorInfo editorInfo = new EditorInfo();
- InputConnection ic = mEditText.onCreateInputConnection(editorInfo);
- return ic.commitContent(contentInfo, 0, null);
- }
-
- private void setTextAndCursor(final String text, final int cursorPosition) {
- mEditText.requestFocus();
- SpannableStringBuilder ssb = new SpannableStringBuilder(text);
- mEditText.setText(ssb);
- mEditText.setSelection(cursorPosition);
- assertThat(mEditText.hasFocus()).isTrue();
- assertTextAndCursorPosition(text, cursorPosition);
- }
-
- private void assertTextAndCursorPosition(String expectedText, int cursorPosition) {
- assertThat(mEditText.getText().toString()).isEqualTo(expectedText);
- assertThat(mEditText.getSelectionStart()).isEqualTo(cursorPosition);
- assertThat(mEditText.getSelectionEnd()).isEqualTo(cursorPosition);
- }
-
- private ClipData copyToClipboard(final ClipData clip) {
- mClipboardManager.setPrimaryClip(clip);
- ClipData primaryClip = mClipboardManager.getPrimaryClip();
- assertThat(primaryClip).isNotNull();
- return primaryClip;
- }
-
- private static ClipData clipEq(ClipData expected) {
- return argThat(new ClipDataArgumentMatcher(expected));
- }
-
- private static class ClipDataArgumentMatcher implements ArgumentMatcher<ClipData> {
- private final ClipData mExpected;
-
- private ClipDataArgumentMatcher(ClipData expected) {
- this.mExpected = expected;
- }
-
- @Override
- public boolean matches(ClipData actual) {
- return mExpected.getItemAt(0).getText().equals(actual.getItemAt(0).getText());
- }
- }
-}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/SwitchCompatTest.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/SwitchCompatTest.java
index 63d04a8..92aba86 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/SwitchCompatTest.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/SwitchCompatTest.java
@@ -24,8 +24,10 @@
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
import android.graphics.Typeface;
+import android.os.Build;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
@@ -87,21 +89,37 @@
AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
switchButton.onInitializeAccessibilityNodeInfo(info);
assertEquals("android.widget.Switch", info.getClassName());
- assertEquals(mActivity.getResources().getString(R.string.sample_text1), info.getText());
- assertEquals(
- mActivity.getResources().getString(androidx.appcompat.R.string.abc_capital_off),
- ViewCompat.getStateDescription(switchButton)
- );
+ final String capitalOff =
+ mActivity.getResources().getString(androidx.appcompat.R.string.abc_capital_off);
+ final String text = mActivity.getResources().getString(R.string.sample_text1);
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ assertEquals(text + " " + capitalOff, info.getText());
+ assertNull(ViewCompat.getStateDescription(switchButton));
+ } else {
+ assertEquals(text, info.getText());
+ assertEquals(capitalOff, ViewCompat.getStateDescription(switchButton));
+ }
+ info.recycle();
}
@Test
public void testAccessibility_textOnOff() {
final SwitchCompat switchButton = mContainer.findViewById(R.id.switch_textOnOff);
+ final CharSequence textOn = "testStateOn";
+ final CharSequence textOff = "testStateOff";
AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
switchButton.onInitializeAccessibilityNodeInfo(info);
assertEquals("android.widget.Switch", info.getClassName());
- assertEquals(mActivity.getResources().getString(R.string.sample_text1), info.getText());
- assertEquals("testStateOff", ViewCompat.getStateDescription(switchButton));
+ final String text = mActivity.getResources().getString(R.string.sample_text1);
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ assertEquals(text + " " + textOff, info.getText());
+ assertNull(ViewCompat.getStateDescription(switchButton));
+ } else {
+ assertEquals(text, info.getText());
+ assertEquals(textOff, ViewCompat.getStateDescription(switchButton));
+ }
+ info.recycle();
+
final CharSequence newTextOff = "new text off";
final CharSequence newTextOn = "new text on";
mActivity.runOnUiThread(
@@ -109,12 +127,41 @@
@Override
public void run() {
switchButton.toggle();
- assertEquals("testStateOn", ViewCompat.getStateDescription(switchButton));
- switchButton.setTextOff(newTextOff);
+ AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
+ switchButton.onInitializeAccessibilityNodeInfo(info);
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ assertEquals(text + " " + textOn, info.getText());
+ assertNull(ViewCompat.getStateDescription(switchButton));
+ } else {
+ assertEquals(text, info.getText());
+ assertEquals(textOn, ViewCompat.getStateDescription(switchButton));
+ }
+ info.recycle();
+
switchButton.setTextOn(newTextOn);
- assertEquals(newTextOn, ViewCompat.getStateDescription(switchButton));
+ switchButton.setTextOff(newTextOff);
+ info = AccessibilityNodeInfo.obtain();
+ switchButton.onInitializeAccessibilityNodeInfo(info);
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ assertEquals(text + " " + newTextOn, info.getText());
+ assertNull(ViewCompat.getStateDescription(switchButton));
+ } else {
+ assertEquals(text, info.getText());
+ assertEquals(newTextOn, ViewCompat.getStateDescription(switchButton));
+ }
+ info.recycle();
+
switchButton.toggle();
- assertEquals(newTextOff, ViewCompat.getStateDescription(switchButton));
+ info = AccessibilityNodeInfo.obtain();
+ switchButton.onInitializeAccessibilityNodeInfo(info);
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ assertEquals(text + " " + newTextOff, info.getText());
+ assertNull(ViewCompat.getStateDescription(switchButton));
+ } else {
+ assertEquals(text, info.getText());
+ assertEquals(newTextOff, ViewCompat.getStateDescription(switchButton));
+ }
+ info.recycle();
}
}
);
diff --git a/appcompat/appcompat/src/androidTest/res/layout/appcompat_edittext_richcontentreceiver_activity.xml b/appcompat/appcompat/src/androidTest/res/layout/appcompat_edittext_receive_content_activity.xml
similarity index 95%
rename from appcompat/appcompat/src/androidTest/res/layout/appcompat_edittext_richcontentreceiver_activity.xml
rename to appcompat/appcompat/src/androidTest/res/layout/appcompat_edittext_receive_content_activity.xml
index b1c4421..c1706b6 100644
--- a/appcompat/appcompat/src/androidTest/res/layout/appcompat_edittext_richcontentreceiver_activity.xml
+++ b/appcompat/appcompat/src/androidTest/res/layout/appcompat_edittext_receive_content_activity.xml
@@ -28,7 +28,7 @@
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatEditText
- android:id="@+id/edit_text_default_values"
+ android:id="@+id/edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
diff --git a/appcompat/appcompat/src/androidTest/res/values/strings.xml b/appcompat/appcompat/src/androidTest/res/values/strings.xml
index 19145b8..563bc0e 100644
--- a/appcompat/appcompat/src/androidTest/res/values/strings.xml
+++ b/appcompat/appcompat/src/androidTest/res/values/strings.xml
@@ -57,7 +57,9 @@
<string name="app_compat_text_view_activity">AppCompat text view</string>
<string name="app_compat_text_view_auto_size_activity">AppCompat text view auto-size</string>
<string name="app_compat_edit_text_activity">AppCompat edit text</string>
- <string name="app_compat_edit_text_rich_content_receiver_activity">AppCompat edit text rich content receiver</string>
+ <string name="app_compat_edit_text_receive_content_activity">
+ AppCompat edit text receive content activity
+ </string>
<string name="app_compat_button_auto_size_activity">AppCompat button auto-size</string>
<string name="sample_text1">Sample text 1</string>
<string name="sample_text2">Sample text 2</string>
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEditText.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEditText.java
index 30e4e6f..8d44a2b 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEditText.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatEditText.java
@@ -17,8 +17,10 @@
package androidx.appcompat.widget;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
-import static androidx.core.widget.RichContentReceiverCompat.FLAG_CONVERT_TO_PLAIN_TEXT;
-import static androidx.core.widget.RichContentReceiverCompat.SOURCE_CLIPBOARD;
+import static androidx.core.view.ContentInfoCompat.Builder;
+import static androidx.core.view.ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT;
+import static androidx.core.view.ContentInfoCompat.SOURCE_CLIPBOARD;
+import static androidx.core.view.ContentInfoCompat.SOURCE_INPUT_METHOD;
import android.content.ClipData;
import android.content.ClipboardManager;
@@ -27,9 +29,12 @@
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Build;
+import android.os.Bundle;
import android.text.Editable;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.ActionMode;
+import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.textclassifier.TextClassifier;
@@ -42,10 +47,17 @@
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.appcompat.R;
+import androidx.core.view.ContentInfoCompat;
+import androidx.core.view.OnReceiveContentListener;
+import androidx.core.view.OnReceiveContentViewBehavior;
import androidx.core.view.TintableBackgroundView;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.inputmethod.EditorInfoCompat;
import androidx.core.view.inputmethod.InputConnectionCompat;
-import androidx.core.widget.RichContentReceiverCompat;
+import androidx.core.view.inputmethod.InputConnectionCompat.OnCommitContentListener;
+import androidx.core.view.inputmethod.InputContentInfoCompat;
import androidx.core.widget.TextViewCompat;
+import androidx.core.widget.TextViewOnReceiveContentListener;
/**
* A {@link EditText} which supports compatible features on older versions of the platform,
@@ -55,8 +67,8 @@
* {@link androidx.core.view.ViewCompat}.</li>
* <li>Allows setting of the background tint using {@link R.attr#backgroundTint} and
* {@link R.attr#backgroundTintMode}.</li>
- * <li>Allows setting a custom {@link RichContentReceiverCompat receiver callback} in order to
- * handle insertion of content (e.g. pasting text or an image from the clipboard). This callback
+ * <li>Allows setting a custom {@link OnReceiveContentListener listener} to handle
+ * insertion of content (e.g. pasting text or an image from the clipboard). This listener
* provides the opportunity to implement app-specific handling such as creating an attachment
* when an image is pasted.</li>
* </ul>
@@ -66,13 +78,14 @@
* <a href="{@docRoot}topic/libraries/support-library/packages.html#v7-appcompat">appcompat</a>.
* You should only need to manually use this class when writing custom views.</p>
*/
-public class AppCompatEditText extends EditText implements TintableBackgroundView {
+public class AppCompatEditText extends EditText implements TintableBackgroundView,
+ OnReceiveContentViewBehavior {
+ private static final String LOG_TAG = "AppCompatEditText";
private final AppCompatBackgroundHelper mBackgroundTintHelper;
private final AppCompatTextHelper mTextHelper;
private final AppCompatTextClassifierHelper mTextClassifierHelper;
- @Nullable
- private RichContentReceiverCompat<TextView> mRichContentReceiverCompat;
+ private final TextViewOnReceiveContentListener mDefaultOnReceiveContentListener;
public AppCompatEditText(@NonNull Context context) {
this(context, null);
@@ -96,6 +109,8 @@
mTextHelper.applyCompoundDrawablesTints();
mTextClassifierHelper = new AppCompatTextClassifierHelper(this);
+
+ mDefaultOnReceiveContentListener = new TextViewOnReceiveContentListener();
}
/**
@@ -204,26 +219,62 @@
}
/**
- * If a {@link #setRichContentReceiverCompat receiver callback} is set, the returned
+ * If a {@link ViewCompat#setOnReceiveContentListener listener is set}, the returned
* {@link InputConnection} will use it to handle calls to {@link InputConnection#commitContent}.
*
* {@inheritDoc}
*/
+ @Nullable
@Override
- public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) {
InputConnection ic = super.onCreateInputConnection(outAttrs);
mTextHelper.populateSurroundingTextIfNeeded(this, ic, outAttrs);
ic = AppCompatHintHelper.onCreateInputConnection(ic, outAttrs, this);
- if (ic != null && mRichContentReceiverCompat != null) {
- mRichContentReceiverCompat.populateEditorInfoContentMimeTypes(ic, outAttrs);
- InputConnectionCompat.OnCommitContentListener callback =
- mRichContentReceiverCompat.buildOnCommitContentListener(this);
- ic = InputConnectionCompat.createWrapper(ic, outAttrs, callback);
+
+ String[] mimeTypes = ViewCompat.getOnReceiveContentMimeTypes(this);
+ if (ic != null && mimeTypes != null) {
+ EditorInfoCompat.setContentMimeTypes(outAttrs, mimeTypes);
+ OnCommitContentListener onCommitContentListener = buildOnCommitContentListener(this);
+ ic = InputConnectionCompat.createWrapper(ic, outAttrs, onCommitContentListener);
}
return ic;
}
/**
+ * Creates an {@link InputConnectionCompat.OnCommitContentListener} that uses
+ * {@link ViewCompat#performReceiveContent} to insert content. The listener returned by this
+ * function should be passed to {@link InputConnectionCompat#createWrapper} when creating the
+ * {@link InputConnection} in {@link View#onCreateInputConnection}.
+ */
+ // TODO(b/150318135): Generalize/extract this so it can be reused for other widgets
+ @NonNull
+ private static InputConnectionCompat.OnCommitContentListener buildOnCommitContentListener(
+ @NonNull final View view) {
+ return new InputConnectionCompat.OnCommitContentListener() {
+ @Override
+ public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags,
+ Bundle opts) {
+ if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
+ try {
+ inputContentInfo.requestPermission();
+ } catch (Exception e) {
+ Log.w(LOG_TAG,
+ "Can't insert content from IME; requestPermission() failed", e);
+ return false;
+ }
+ }
+ ClipData clip = new ClipData(inputContentInfo.getDescription(),
+ new ClipData.Item(inputContentInfo.getContentUri()));
+ ContentInfoCompat payload = new ContentInfoCompat.Builder(clip, SOURCE_INPUT_METHOD)
+ .setLinkUri(inputContentInfo.getLinkUri())
+ .setExtras(opts)
+ .build();
+ return ViewCompat.performReceiveContent(view, payload) == null;
+ }
+ };
+ }
+
+ /**
* See
* {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
*/
@@ -264,23 +315,23 @@
}
/**
- * If a {@link #setRichContentReceiverCompat receiver callback} is set, uses it to execute the
+ * If a {@link ViewCompat#setOnReceiveContentListener listener is set}, uses it to execute the
* "Paste" and "Paste as plain text" menu actions.
*
* {@inheritDoc}
*/
@Override
public boolean onTextContextMenuItem(int id) {
- if (mRichContentReceiverCompat == null) {
- return super.onTextContextMenuItem(id);
- }
- if (id == android.R.id.paste || id == android.R.id.pasteAsPlainText) {
+ if (ViewCompat.getOnReceiveContentMimeTypes(this) != null
+ && (id == android.R.id.paste || id == android.R.id.pasteAsPlainText)) {
ClipboardManager cm = (ClipboardManager) getContext().getSystemService(
Context.CLIPBOARD_SERVICE);
- ClipData clip = cm == null ? null : cm.getPrimaryClip();
+ ClipData clip = (cm == null) ? null : cm.getPrimaryClip();
if (clip != null) {
- int flags = (id == android.R.id.paste) ? 0 : FLAG_CONVERT_TO_PLAIN_TEXT;
- mRichContentReceiverCompat.onReceive(this, clip, SOURCE_CLIPBOARD, flags);
+ ContentInfoCompat payload = new Builder(clip, SOURCE_CLIPBOARD)
+ .setFlags((id == android.R.id.paste) ? 0 : FLAG_CONVERT_TO_PLAIN_TEXT)
+ .build();
+ ViewCompat.performReceiveContent(this, payload);
}
return true;
}
@@ -288,39 +339,23 @@
}
/**
- * Returns the callback that handles insertion of content into this view (e.g. pasting from
- * the clipboard). See {@link #setRichContentReceiverCompat} for more info.
+ * Implements the default behavior for receiving content, which coerces all content to text
+ * and inserts into the view.
*
- * @return The callback that this view is using to handle insertion of content. Returns
- * {@code null} if no callback is configured, in which case the platform behavior of the
- * {@link EditText} component will be used for content insertion.
+ * <p>Subclasses of this widget can override this method to customize the default behavior
+ * for receiving content. Apps wishing to provide custom behavior for receiving content
+ * should set a listener via {@link ViewCompat#setOnReceiveContentListener}.
+ *
+ * <p>See {@link ViewCompat#performReceiveContent} for more info.
+ *
+ * @param payload The content to insert and related metadata.
+ *
+ * @return The portion of the passed-in content that was not handled (may be all, some, or none
+ * of the passed-in content).
*/
@Nullable
- public RichContentReceiverCompat<TextView> getRichContentReceiverCompat() {
- return mRichContentReceiverCompat;
- }
-
- /**
- * Sets the callback to handle insertion of content into this view.
- *
- * <p>"Content" and "rich content" here refers to both text and non-text: plain text, styled
- * text, HTML, images, videos, audio files, etc. The callback configured here should typically
- * extend from {@link androidx.core.widget.TextViewRichContentReceiverCompat} to provide
- * consistent behavior for text content.
- *
- * <p>This callback will be invoked for the following 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 ({@link InputConnection#commitContent})
- * </ol>
- *
- * @param receiver The callback to use. This can be {@code null} to clear any previously set
- * callback (the platform behavior of the {@link EditText} component will then
- * be used).
- */
- public void setRichContentReceiverCompat(
- @Nullable RichContentReceiverCompat<TextView> receiver) {
- mRichContentReceiverCompat = receiver;
+ @Override
+ public ContentInfoCompat onReceiveContent(@NonNull ContentInfoCompat payload) {
+ return mDefaultOnReceiveContentListener.onReceiveContent(this, payload);
}
}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SwitchCompat.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SwitchCompat.java
index 5e25a81..f4d72a52 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SwitchCompat.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/SwitchCompat.java
@@ -775,9 +775,11 @@
public void setTextOn(CharSequence textOn) {
mTextOn = textOn;
requestLayout();
- // Default state is derived from on/off-text, so state has to be updated when on/off-text
- // are updated.
- setOnStateDescription();
+ if (isChecked()) {
+ // Default state is derived from on/off-text, so state has to be updated when
+ // on/off-text are updated.
+ setOnStateDescriptionOnRAndAbove();
+ }
}
/**
@@ -797,9 +799,11 @@
public void setTextOff(CharSequence textOff) {
mTextOff = textOff;
requestLayout();
- // Default state is derived from on/off-text, so state has to be updated when on/off-text
- // are updated.
- setOffStateDescription();
+ if (!isChecked()) {
+ // Default state is derived from on/off-text, so state has to be updated when
+ // on/off-text are updated.
+ setOffStateDescriptionOnRAndAbove();
+ }
}
/**
@@ -1095,9 +1099,9 @@
checked = isChecked();
if (checked) {
- setOnStateDescription();
+ setOnStateDescriptionOnRAndAbove();
} else {
- setOffStateDescription();
+ setOffStateDescriptionOnRAndAbove();
}
if (getWindowToken() != null && ViewCompat.isLaidOut(this)) {
@@ -1433,6 +1437,19 @@
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
info.setClassName(ACCESSIBILITY_EVENT_CLASS_NAME);
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ CharSequence switchText = isChecked() ? mTextOn : mTextOff;
+ if (!TextUtils.isEmpty(switchText)) {
+ CharSequence oldText = info.getText();
+ if (TextUtils.isEmpty(oldText)) {
+ info.setText(switchText);
+ } else {
+ StringBuilder newText = new StringBuilder();
+ newText.append(oldText).append(' ').append(switchText);
+ info.setText(newText);
+ }
+ }
+ }
}
/**
@@ -1452,17 +1469,21 @@
return amount < low ? low : (amount > high ? high : amount);
}
- private void setOnStateDescription() {
- ViewCompat.setStateDescription(
- this,
- mTextOn == null ? getResources().getString(R.string.abc_capital_on) : mTextOn
- );
+ private void setOnStateDescriptionOnRAndAbove() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ ViewCompat.setStateDescription(
+ this,
+ mTextOn == null ? getResources().getString(R.string.abc_capital_on) : mTextOn
+ );
+ }
}
- private void setOffStateDescription() {
- ViewCompat.setStateDescription(
- this,
- mTextOff == null ? getResources().getString(R.string.abc_capital_off) : mTextOff
- );
+ private void setOffStateDescriptionOnRAndAbove() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ ViewCompat.setStateDescription(
+ this,
+ mTextOff == null ? getResources().getString(R.string.abc_capital_off) : mTextOff
+ );
+ }
}
}
diff --git a/appsearch/appsearch/build.gradle b/appsearch/appsearch/build.gradle
index 907de5a..085f8ad 100644
--- a/appsearch/appsearch/build.gradle
+++ b/appsearch/appsearch/build.gradle
@@ -56,8 +56,8 @@
def suffix = name.capitalize()
project.tasks.create(name: "jar${suffix}", type: Jar) {
dependsOn variant.javaCompileProvider.get()
- from variant.javaCompileProvider.get().destinationDir
- destinationDir new File(project.buildDir, "libJar")
+ from variant.javaCompileProvider.get().destinationDirectory
+ destinationDirectory.set(new File(project.buildDir, "libJar"))
}
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTest.java
index 6edc6c1..bb81bfd 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTest.java
@@ -27,9 +27,11 @@
import android.content.Context;
import androidx.appsearch.annotation.AppSearchDocument;
+import androidx.appsearch.app.util.AppSearchTestUtils;
import androidx.appsearch.localstorage.LocalStorage;
import androidx.test.core.app.ApplicationProvider;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -43,18 +45,22 @@
@Before
public void setUp() throws Exception {
Context context = ApplicationProvider.getApplicationContext();
+ AppSearchTestUtils.cleanup(context);
+
mSession = checkIsResultSuccess(LocalStorage.createSearchSession(
new LocalStorage.SearchContext.Builder(context)
- .setDatabaseName("testDb1").build()));
+ .setDatabaseName(AppSearchTestUtils.DB_1).build()));
+ }
- // Remove all documents from any instances that may have been created in the tests.
- checkIsResultSuccess(
- mSession.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()));
+ @After
+ public void tearDown() throws Exception {
+ AppSearchTestUtils.cleanup(ApplicationProvider.getApplicationContext());
}
@AppSearchDocument
static class Card {
- @AppSearchDocument.Uri String mUri;
+ @AppSearchDocument.Uri
+ String mUri;
@AppSearchDocument.Property
(indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
String mString; // 3a
@@ -75,48 +81,84 @@
@AppSearchDocument
static class Gift {
- @AppSearchDocument.Uri String mUri;
+ @AppSearchDocument.Uri
+ String mUri;
// Collections
- @AppSearchDocument.Property Collection<Long> mCollectLong; // 1a
- @AppSearchDocument.Property Collection<Integer> mCollectInteger; // 1a
- @AppSearchDocument.Property Collection<Double> mCollectDouble; // 1a
- @AppSearchDocument.Property Collection<Float> mCollectFloat; // 1a
- @AppSearchDocument.Property Collection<Boolean> mCollectBoolean; // 1a
- @AppSearchDocument.Property Collection<byte[]> mCollectByteArr; // 1a
- @AppSearchDocument.Property Collection<String> mCollectString; // 1b
- @AppSearchDocument.Property Collection<Card> mCollectCard; // 1c
+ @AppSearchDocument.Property
+ Collection<Long> mCollectLong; // 1a
+ @AppSearchDocument.Property
+ Collection<Integer> mCollectInteger; // 1a
+ @AppSearchDocument.Property
+ Collection<Double> mCollectDouble; // 1a
+ @AppSearchDocument.Property
+ Collection<Float> mCollectFloat; // 1a
+ @AppSearchDocument.Property
+ Collection<Boolean> mCollectBoolean; // 1a
+ @AppSearchDocument.Property
+ Collection<byte[]> mCollectByteArr; // 1a
+ @AppSearchDocument.Property
+ Collection<String> mCollectString; // 1b
+ @AppSearchDocument.Property
+ Collection<Card> mCollectCard; // 1c
// Arrays
- @AppSearchDocument.Property Long[] mArrBoxLong; // 2a
- @AppSearchDocument.Property long[] mArrUnboxLong; // 2b
- @AppSearchDocument.Property Integer[] mArrBoxInteger; // 2a
- @AppSearchDocument.Property int[] mArrUnboxInt; // 2a
- @AppSearchDocument.Property Double[] mArrBoxDouble; // 2a
- @AppSearchDocument.Property double[] mArrUnboxDouble; // 2b
- @AppSearchDocument.Property Float[] mArrBoxFloat; // 2a
- @AppSearchDocument.Property float[] mArrUnboxFloat; // 2a
- @AppSearchDocument.Property Boolean[] mArrBoxBoolean; // 2a
- @AppSearchDocument.Property boolean[] mArrUnboxBoolean; // 2b
- @AppSearchDocument.Property byte[][] mArrUnboxByteArr; // 2b
- @AppSearchDocument.Property Byte[] mBoxByteArr; // 2a
- @AppSearchDocument.Property String[] mArrString; // 2b
- @AppSearchDocument.Property Card[] mArrCard; // 2c
+ @AppSearchDocument.Property
+ Long[] mArrBoxLong; // 2a
+ @AppSearchDocument.Property
+ long[] mArrUnboxLong; // 2b
+ @AppSearchDocument.Property
+ Integer[] mArrBoxInteger; // 2a
+ @AppSearchDocument.Property
+ int[] mArrUnboxInt; // 2a
+ @AppSearchDocument.Property
+ Double[] mArrBoxDouble; // 2a
+ @AppSearchDocument.Property
+ double[] mArrUnboxDouble; // 2b
+ @AppSearchDocument.Property
+ Float[] mArrBoxFloat; // 2a
+ @AppSearchDocument.Property
+ float[] mArrUnboxFloat; // 2a
+ @AppSearchDocument.Property
+ Boolean[] mArrBoxBoolean; // 2a
+ @AppSearchDocument.Property
+ boolean[] mArrUnboxBoolean; // 2b
+ @AppSearchDocument.Property
+ byte[][] mArrUnboxByteArr; // 2b
+ @AppSearchDocument.Property
+ Byte[] mBoxByteArr; // 2a
+ @AppSearchDocument.Property
+ String[] mArrString; // 2b
+ @AppSearchDocument.Property
+ Card[] mArrCard; // 2c
// Single values
- @AppSearchDocument.Property String mString; // 3a
- @AppSearchDocument.Property Long mBoxLong; // 3a
- @AppSearchDocument.Property long mUnboxLong; // 3b
- @AppSearchDocument.Property Integer mBoxInteger; // 3a
- @AppSearchDocument.Property int mUnboxInt; // 3b
- @AppSearchDocument.Property Double mBoxDouble; // 3a
- @AppSearchDocument.Property double mUnboxDouble; // 3b
- @AppSearchDocument.Property Float mBoxFloat; // 3a
- @AppSearchDocument.Property float mUnboxFloat; // 3b
- @AppSearchDocument.Property Boolean mBoxBoolean; // 3a
- @AppSearchDocument.Property boolean mUnboxBoolean; // 3b
- @AppSearchDocument.Property byte[] mUnboxByteArr; // 3a
- @AppSearchDocument.Property Card mCard; // 3c
+ @AppSearchDocument.Property
+ String mString; // 3a
+ @AppSearchDocument.Property
+ Long mBoxLong; // 3a
+ @AppSearchDocument.Property
+ long mUnboxLong; // 3b
+ @AppSearchDocument.Property
+ Integer mBoxInteger; // 3a
+ @AppSearchDocument.Property
+ int mUnboxInt; // 3b
+ @AppSearchDocument.Property
+ Double mBoxDouble; // 3a
+ @AppSearchDocument.Property
+ double mUnboxDouble; // 3b
+ @AppSearchDocument.Property
+ Float mBoxFloat; // 3a
+ @AppSearchDocument.Property
+ float mUnboxFloat; // 3b
+ @AppSearchDocument.Property
+ Boolean mBoxBoolean; // 3a
+ @AppSearchDocument.Property
+ boolean mUnboxBoolean; // 3b
+ @AppSearchDocument.Property
+ byte[] mUnboxByteArr; // 3a
+ @AppSearchDocument.Property
+ Card mCard; // 3c
@Override
public boolean equals(Object other) {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
index 99ac18e..746216d 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
@@ -24,9 +24,14 @@
import static org.junit.Assert.assertThrows;
import androidx.appsearch.annotation.AppSearchDocument;
+import androidx.collection.ArrayMap;
import org.junit.Test;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
public class SetSchemaRequestTest {
@AppSearchDocument
@@ -52,15 +57,24 @@
}
@Test
- public void testInvalidSchemaReferences() {
+ public void testInvalidSchemaReferences_fromSystemUiVisibility() {
IllegalArgumentException expected = assertThrows(IllegalArgumentException.class,
- () -> new SetSchemaRequest.Builder().setSchemaTypeVisibilityForSystemUi(false,
- "InvalidSchema").build());
+ () -> new SetSchemaRequest.Builder().setSchemaTypeVisibilityForSystemUi(
+ "InvalidSchema", false).build());
assertThat(expected).hasMessageThat().contains("referenced, but were not added");
}
@Test
- public void testSchemaTypeVisibilityForSystemUi_Visible() {
+ public void testInvalidSchemaReferences_fromPackageVisibility() {
+ IllegalArgumentException expected = assertThrows(IllegalArgumentException.class,
+ () -> new SetSchemaRequest.Builder().setSchemaTypeVisibilityForPackage(
+ "InvalidSchema", /*visible=*/ true, new PackageIdentifier(
+ "com.foo.package", /*certificate=*/ new byte[]{})).build());
+ assertThat(expected).hasMessageThat().contains("referenced, but were not added");
+ }
+
+ @Test
+ public void testSchemaTypeVisibilityForSystemUi_visible() {
AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
// By default, the schema is visible.
@@ -70,23 +84,21 @@
request =
new SetSchemaRequest.Builder().addSchema(schema).setSchemaTypeVisibilityForSystemUi(
- true,
- "Schema").build();
+ "Schema", true).build();
assertThat(request.getSchemasNotPlatformSurfaceable()).isEmpty();
}
@Test
- public void testSchemaTypeVisibilityForSystemUi_NotVisible() {
+ public void testSchemaTypeVisibilityForSystemUi_notVisible() {
AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
SetSchemaRequest request =
new SetSchemaRequest.Builder().addSchema(schema).setSchemaTypeVisibilityForSystemUi(
- false,
- "Schema").build();
+ "Schema", false).build();
assertThat(request.getSchemasNotPlatformSurfaceable()).containsExactly("Schema");
}
@Test
- public void testDataClassVisibilityForSystemUi_Visible() throws Exception {
+ public void testDataClassVisibilityForSystemUi_visible() throws Exception {
// By default, the schema is visible.
SetSchemaRequest request =
new SetSchemaRequest.Builder().addDataClass(Card.class).build();
@@ -95,18 +107,171 @@
request =
new SetSchemaRequest.Builder().addDataClass(
Card.class).setDataClassVisibilityForSystemUi(
- true,
- Card.class).build();
+ Card.class, true).build();
assertThat(request.getSchemasNotPlatformSurfaceable()).isEmpty();
}
@Test
- public void testDataClassVisibilityForSystemUi_NotVisible() throws Exception {
+ public void testDataClassVisibilityForSystemUi_notVisible() throws Exception {
SetSchemaRequest request =
new SetSchemaRequest.Builder().addDataClass(
Card.class).setDataClassVisibilityForSystemUi(
- false,
- Card.class).build();
+ Card.class, false).build();
assertThat(request.getSchemasNotPlatformSurfaceable()).containsExactly("Card");
}
+
+ @Test
+ public void testSchemaTypeVisibilityForPackage_visible() {
+ AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+ // By default, the schema is not visible.
+ SetSchemaRequest request =
+ new SetSchemaRequest.Builder().addSchema(schema).build();
+ assertThat(request.getSchemasPackageAccessible()).isEmpty();
+
+ PackageIdentifier packageIdentifier = new PackageIdentifier("com.package.foo",
+ new byte[]{100});
+ Map<String, Set<PackageIdentifier>> expectedPackageVisibleMap = new ArrayMap<>();
+ expectedPackageVisibleMap.put("Schema", Collections.singleton(packageIdentifier));
+
+ request =
+ new SetSchemaRequest.Builder().addSchema(schema).setSchemaTypeVisibilityForPackage(
+ "Schema", /*visible=*/ true, packageIdentifier).build();
+ assertThat(request.getSchemasPackageAccessible()).containsExactlyEntriesIn(
+ expectedPackageVisibleMap);
+ }
+
+ @Test
+ public void testSchemaTypeVisibilityForPackage_notVisible() {
+ AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+ SetSchemaRequest request =
+ new SetSchemaRequest.Builder().addSchema(schema).setSchemaTypeVisibilityForPackage(
+ "Schema", /*visible=*/ false, new PackageIdentifier("com.package.foo",
+ /*certificate=*/ new byte[]{})).build();
+ assertThat(request.getSchemasPackageAccessible()).isEmpty();
+ }
+
+ @Test
+ public void testSchemaTypeVisibilityForPackage_deduped() throws Exception {
+ AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+ PackageIdentifier packageIdentifier = new PackageIdentifier("com.package.foo",
+ new byte[]{100});
+ Map<String, Set<PackageIdentifier>> expectedPackageVisibleMap = new ArrayMap<>();
+ expectedPackageVisibleMap.put("Schema", Collections.singleton(packageIdentifier));
+
+ SetSchemaRequest request =
+ new SetSchemaRequest.Builder()
+ .addSchema(schema)
+ // Set it visible for "Schema"
+ .setSchemaTypeVisibilityForPackage("Schema", /*visible=*/
+ true, packageIdentifier)
+ // Set it visible for "Schema" again, which should be a no-op
+ .setSchemaTypeVisibilityForPackage("Schema", /*visible=*/
+ true, packageIdentifier)
+ .build();
+ assertThat(request.getSchemasPackageAccessible()).containsExactlyEntriesIn(
+ expectedPackageVisibleMap);
+ }
+
+ @Test
+ public void testSchemaTypeVisibilityForPackage_removed() throws Exception {
+ AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+ SetSchemaRequest request =
+ new SetSchemaRequest.Builder()
+ .addSchema(schema)
+ // First set it as visible
+ .setSchemaTypeVisibilityForPackage("Schema", /*visible=*/
+ true, new PackageIdentifier("com.package.foo",
+ /*certificate=*/ new byte[]{100}))
+ // Then make it not visible
+ .setSchemaTypeVisibilityForPackage("Schema", /*visible=*/
+ false, new PackageIdentifier("com.package.foo",
+ /*certificate=*/ new byte[]{100}))
+ .build();
+
+ // Nothing should be visible.
+ assertThat(request.getSchemasPackageAccessible()).isEmpty();
+ }
+
+ @Test
+ public void testDataClassVisibilityForPackage_visible() throws Exception {
+ // By default, the schema is not visible.
+ SetSchemaRequest request =
+ new SetSchemaRequest.Builder().addDataClass(Card.class).build();
+ assertThat(request.getSchemasPackageAccessible()).isEmpty();
+
+ PackageIdentifier packageIdentifier = new PackageIdentifier("com.package.foo",
+ new byte[]{100});
+ Map<String, Set<PackageIdentifier>> expectedPackageVisibleMap = new ArrayMap<>();
+ expectedPackageVisibleMap.put("Card", Collections.singleton(packageIdentifier));
+
+ request =
+ new SetSchemaRequest.Builder().addDataClass(
+ Card.class).setDataClassVisibilityForPackage(
+ Card.class, /*visible=*/ true, packageIdentifier).build();
+ assertThat(request.getSchemasPackageAccessible()).containsExactlyEntriesIn(
+ expectedPackageVisibleMap);
+ }
+
+ @Test
+ public void testDataClassVisibilityForPackage_notVisible() throws Exception {
+ SetSchemaRequest request =
+ new SetSchemaRequest.Builder().addDataClass(
+ Card.class).setDataClassVisibilityForPackage(
+ Card.class, /*visible=*/ false,
+ new PackageIdentifier("com.package.foo", /*certificate=*/
+ new byte[]{})).build();
+ assertThat(request.getSchemasPackageAccessible()).isEmpty();
+ }
+
+ @Test
+ public void testDataClassVisibilityForPackage_deduped() throws Exception {
+ // By default, the schema is not visible.
+ SetSchemaRequest request =
+ new SetSchemaRequest.Builder().addDataClass(Card.class).build();
+ assertThat(request.getSchemasPackageAccessible()).isEmpty();
+
+ PackageIdentifier packageIdentifier = new PackageIdentifier("com.package.foo",
+ new byte[]{100});
+ Map<String, Set<PackageIdentifier>> expectedPackageVisibleMap = new ArrayMap<>();
+ expectedPackageVisibleMap.put("Card", Collections.singleton(packageIdentifier));
+
+ request =
+ new SetSchemaRequest.Builder()
+ .addDataClass(Card.class)
+ .setDataClassVisibilityForPackage(Card.class, /*visible=*/
+ true, packageIdentifier)
+ .setDataClassVisibilityForPackage(Card.class, /*visible=*/
+ true, packageIdentifier)
+ .build();
+ assertThat(request.getSchemasPackageAccessible()).containsExactlyEntriesIn(
+ expectedPackageVisibleMap);
+ }
+
+ @Test
+ public void testDataClassVisibilityForPackage_removed() throws Exception {
+ // By default, the schema is not visible.
+ SetSchemaRequest request =
+ new SetSchemaRequest.Builder().addDataClass(Card.class).build();
+ assertThat(request.getSchemasPackageAccessible()).isEmpty();
+
+ request =
+ new SetSchemaRequest.Builder()
+ .addDataClass(Card.class)
+ // First set it as visible
+ .setDataClassVisibilityForPackage(Card.class, /*visible=*/
+ true, new PackageIdentifier("com.package.foo",
+ /*certificate=*/ new byte[]{100}))
+ // Then make it not visible
+ .setDataClassVisibilityForPackage(Card.class, /*visible=*/
+ false, new PackageIdentifier("com.package.foo",
+ /*certificate=*/ new byte[]{100}))
+ .build();
+
+ // Nothing should be visible.
+ assertThat(request.getSchemasPackageAccessible()).isEmpty();
+ }
}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java
index 91af24c..925cff2 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java
@@ -40,9 +40,11 @@
import androidx.appsearch.app.SearchSpec;
import androidx.appsearch.app.SetSchemaRequest;
import androidx.appsearch.app.cts.customer.EmailDataClass;
+import androidx.appsearch.app.util.AppSearchTestUtils;
import androidx.appsearch.localstorage.LocalStorage;
import androidx.test.core.app.ApplicationProvider;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -53,23 +55,26 @@
public class AppSearchSessionCtsTest {
private AppSearchSession mDb1;
+ private final String mDbName1 = AppSearchTestUtils.DEFAULT_DATABASE;
private AppSearchSession mDb2;
+ private final String mDbName2 = AppSearchTestUtils.DB_2;
@Before
public void setUp() throws Exception {
Context context = ApplicationProvider.getApplicationContext();
+ AppSearchTestUtils.cleanup(context);
+
mDb1 = checkIsResultSuccess(LocalStorage.createSearchSession(
new LocalStorage.SearchContext.Builder(context)
- .setDatabaseName("testDb1").build()));
+ .setDatabaseName(mDbName1).build()));
mDb2 = checkIsResultSuccess(LocalStorage.createSearchSession(
new LocalStorage.SearchContext.Builder(context)
- .setDatabaseName("testDb2").build()));
+ .setDatabaseName(mDbName2).build()));
+ }
- // Remove all documents from any instances that may have been created in the tests.
- checkIsResultSuccess(
- mDb1.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()));
- checkIsResultSuccess(
- mDb2.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()));
+ @After
+ public void tearDown() throws Exception {
+ AppSearchTestUtils.cleanup(ApplicationProvider.getApplicationContext());
}
@Test
@@ -284,7 +289,7 @@
assertThat(failResult1.isSuccess()).isFalse();
assertThat(failResult1.getErrorMessage()).contains("Schema is incompatible");
assertThat(failResult1.getErrorMessage())
- .contains("Deleted types: [testDb1/builtin:Email]");
+ .contains("Deleted types: [" + mDbName1 + "/builtin:Email]");
// Try to remove the email schema again, which should now work as we set forceOverride to
// be true.
@@ -307,10 +312,10 @@
.setSubject("testPut example")
.build();
AppSearchBatchResult<String, Void> failResult2 = mDb1.putDocuments(
- new PutDocumentsRequest.Builder().addGenericDocument(email2).build()).get();
+ new PutDocumentsRequest.Builder().addGenericDocument(email2).build()).get();
assertThat(failResult2.isSuccess()).isFalse();
assertThat(failResult2.getFailures().get("email2").getErrorMessage())
- .isEqualTo("Schema type config 'testDb1/builtin:Email' not found");
+ .isEqualTo("Schema type config '" + mDbName1 + "/builtin:Email' not found");
}
@Test
@@ -363,7 +368,7 @@
assertThat(failResult1.isSuccess()).isFalse();
assertThat(failResult1.getErrorMessage()).contains("Schema is incompatible");
assertThat(failResult1.getErrorMessage())
- .contains("Deleted types: [testDb1/builtin:Email]");
+ .contains("Deleted types: [" + mDbName1 + "/builtin:Email]");
// Try to remove the email schema again, which should now work as we set forceOverride to
// be true.
@@ -387,7 +392,7 @@
new PutDocumentsRequest.Builder().addGenericDocument(email3).build()).get();
assertThat(failResult2.isSuccess()).isFalse();
assertThat(failResult2.getFailures().get("email3").getErrorMessage())
- .isEqualTo("Schema type config 'testDb1/builtin:Email' not found");
+ .isEqualTo("Schema type config '" + mDbName1 + "/builtin:Email' not found");
// Make sure email in database 2 still present.
outDocuments = doGet(mDb2, GenericDocument.DEFAULT_NAMESPACE, "email2");
@@ -698,7 +703,7 @@
new GenericDocument.Builder<>("uri", "Generic")
.setNamespace("document")
.setPropertyString("subject", "A commonly used fake word is foo. "
- + "Another nonsense word that’s used a lot is bar")
+ + "Another nonsense word that’s used a lot is bar")
.build();
checkIsBatchResultSuccess(mDb1.putDocuments(
new PutDocumentsRequest.Builder().addGenericDocument(document).build()));
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionCtsTest.java
index cc1a8bf..ad77545 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionCtsTest.java
@@ -35,9 +35,11 @@
import androidx.appsearch.app.SearchResults;
import androidx.appsearch.app.SearchSpec;
import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.util.AppSearchTestUtils;
import androidx.appsearch.localstorage.LocalStorage;
import androidx.test.core.app.ApplicationProvider;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -55,21 +57,23 @@
@Before
public void setUp() throws Exception {
Context context = ApplicationProvider.getApplicationContext();
+ AppSearchTestUtils.cleanup(context);
+
mDb1 = checkIsResultSuccess(LocalStorage.createSearchSession(
new LocalStorage.SearchContext.Builder(context)
- .setDatabaseName("testDb1").build()));
+ .setDatabaseName(AppSearchTestUtils.DEFAULT_DATABASE).build()));
mDb2 = checkIsResultSuccess(LocalStorage.createSearchSession(
new LocalStorage.SearchContext.Builder(context)
- .setDatabaseName("testDb2").build()));
+ .setDatabaseName(AppSearchTestUtils.DB_2).build()));
mGlobalAppSearchManager = checkIsResultSuccess(LocalStorage.createGlobalSearchSession(
new LocalStorage.GlobalSearchContext.Builder(context).build()));
- // Remove all documents from any instances that may have been created in the tests.
- checkIsResultSuccess(
- mDb1.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()));
- checkIsResultSuccess(
- mDb2.setSchema(new SetSchemaRequest.Builder().setForceOverride(true).build()));
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ AppSearchTestUtils.cleanup(ApplicationProvider.getApplicationContext());
}
@Test
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/util/AppSearchTestUtils.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/util/AppSearchTestUtils.java
index fe176c8..0c2a856 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/util/AppSearchTestUtils.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/util/AppSearchTestUtils.java
@@ -18,6 +18,8 @@
import static com.google.common.truth.Truth.assertThat;
+import android.content.Context;
+
import androidx.appsearch.app.AppSearchBatchResult;
import androidx.appsearch.app.AppSearchResult;
import androidx.appsearch.app.AppSearchSession;
@@ -25,6 +27,10 @@
import androidx.appsearch.app.GetByUriRequest;
import androidx.appsearch.app.SearchResult;
import androidx.appsearch.app.SearchResults;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.localstorage.LocalStorage;
+
+import com.google.common.collect.ImmutableList;
import junit.framework.AssertionFailedError;
@@ -34,6 +40,24 @@
public class AppSearchTestUtils {
+ // List of databases that may be used in tests. Keeping them in a centralized location helps
+ // #cleanup know which databases to clear.
+ public static final String DEFAULT_DATABASE = LocalStorage.DEFAULT_DATABASE_NAME;
+ public static final String DB_1 = "testDb1";
+ public static final String DB_2 = "testDb2";
+
+ public static void cleanup(Context context) throws Exception {
+ List<String> databases = ImmutableList.of(DEFAULT_DATABASE, DB_1, DB_2);
+ for (String database : databases) {
+ AppSearchSession session =
+ checkIsResultSuccess(
+ LocalStorage.createSearchSession(new LocalStorage.SearchContext.Builder(
+ context).setDatabaseName(database).build()));
+ checkIsResultSuccess(session.setSchema(
+ new SetSchemaRequest.Builder().setForceOverride(true).build()));
+ }
+ }
+
public static <V> V checkIsResultSuccess(Future<AppSearchResult<V>> future) throws Exception {
AppSearchResult<V> result = future.get();
if (!result.isSuccess()) {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java
index 3830b9d..389c3ee 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java
@@ -39,7 +39,6 @@
public void testDeepEquals_self() {
Bundle one = new Bundle();
one.putString("a", "a");
- assertThat(one).isEqualTo(one);
assertThat(BundleUtil.deepEquals(one, one)).isTrue();
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
index ffc73249..0a46469 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
@@ -192,11 +192,12 @@
/**
* Removes {@link GenericDocument}s from the index by Query. Documents will be removed if they
- * match the query expression in given namespaces and schemaTypes.
+ * match the {@code queryExpression} in given namespaces and schemaTypes which is set via
+ * {@link SearchSpec.Builder#addNamespace} and {@link SearchSpec.Builder#addSchemaType}.
*
- * <p> An empty query matches all documents.
+ * <p> An empty {@code queryExpression} matches all documents.
*
- * <p> An empty set of namespaces or of schemaTypes matches all namespaces or schemaTypes in
+ * <p> An empty set of namespaces or schemaTypes matches all namespaces or schemaTypes in
* the current database.
*
* @param queryExpression Query String to search.
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java
new file mode 100644
index 0000000..a1ab3b5
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/PackageIdentifier.java
@@ -0,0 +1,63 @@
+/*
+ * 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.appsearch.app;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.ObjectsCompat;
+
+import java.util.Arrays;
+
+/**
+ * This class represents a uniquely identifiable package.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class PackageIdentifier {
+ public final String packageName;
+ public final byte[] certificate;
+
+ /**
+ * Creates a unique identifier for a package.
+ *
+ * @param packageName Name of the package.
+ * @param certificate SHA256 certificate digest of the package.
+ */
+ public PackageIdentifier(@NonNull String packageName, @NonNull byte[] certificate) {
+ this.packageName = packageName;
+ this.certificate = certificate;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || !(obj instanceof PackageIdentifier)) {
+ return false;
+ }
+ final PackageIdentifier other = (PackageIdentifier) obj;
+ return this.packageName.equals(other.packageName)
+ && Arrays.equals(this.certificate, other.certificate);
+ }
+
+ @Override
+ public int hashCode() {
+ return ObjectsCompat.hash(packageName, Arrays.hashCode(certificate));
+ }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByUriRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByUriRequest.java
index 4093c8b..ed7cad9 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByUriRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/RemoveByUriRequest.java
@@ -45,7 +45,7 @@
return mNamespace;
}
- /** Returns the URIs to remove from the namespace. */
+ /** Returns the URIs of documents to remove from the namespace. */
@NonNull
public Set<String> getUris() {
return Collections.unmodifiableSet(mUris);
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
index a918106..04be1e0 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
@@ -21,6 +21,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.appsearch.exceptions.AppSearchException;
+import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
@@ -29,6 +30,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -39,12 +41,16 @@
public final class SetSchemaRequest {
private final Set<AppSearchSchema> mSchemas;
private final Set<String> mSchemasNotPlatformSurfaceable;
+ private final Map<String, Set<PackageIdentifier>> mSchemasPackageAccessible;
private final boolean mForceOverride;
SetSchemaRequest(@NonNull Set<AppSearchSchema> schemas,
- @NonNull Set<String> schemasNotPlatformSurfaceable, boolean forceOverride) {
+ @NonNull Set<String> schemasNotPlatformSurfaceable,
+ @NonNull Map<String, Set<PackageIdentifier>> schemasPackageAccessible,
+ boolean forceOverride) {
mSchemas = Preconditions.checkNotNull(schemas);
mSchemasNotPlatformSurfaceable = Preconditions.checkNotNull(schemasNotPlatformSurfaceable);
+ mSchemasPackageAccessible = Preconditions.checkNotNull(schemasPackageAccessible);
mForceOverride = forceOverride;
}
@@ -65,6 +71,42 @@
return Collections.unmodifiableSet(mSchemasNotPlatformSurfaceable);
}
+ /**
+ * Returns a mapping of schema types to the set of packages that have access
+ * to that schema type. Each package is represented by a {@link PackageIdentifier}.
+ * name and byte[] certificate.
+ *
+ * This method is inefficient to call repeatedly.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public Map<String, Set<PackageIdentifier>> getSchemasPackageAccessible() {
+ Map<String, Set<PackageIdentifier>> copy = new ArrayMap<>();
+ for (String key : mSchemasPackageAccessible.keySet()) {
+ copy.put(key, new ArraySet<>(mSchemasPackageAccessible.get(key)));
+ }
+ return copy;
+ }
+
+ /**
+ * Returns a mapping of schema types to the set of packages that have access
+ * to that schema type. Each package is represented by a {@link PackageIdentifier}.
+ * name and byte[] certificate.
+ *
+ * A more efficient version of {@code #getSchemasPackageAccessible}, but it returns a
+ * modifiable map. This is not meant to be unhidden and should only be used by internal
+ * classes.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public Map<String, Set<PackageIdentifier>> getSchemasPackageAccessibleInternal() {
+ return mSchemasPackageAccessible;
+ }
+
/** Returns whether this request will force the schema to be overridden. */
public boolean isForceOverride() {
return mForceOverride;
@@ -74,6 +116,8 @@
public static final class Builder {
private final Set<AppSearchSchema> mSchemas = new ArraySet<>();
private final Set<String> mSchemasNotPlatformSurfaceable = new ArraySet<>();
+ private final Map<String, Set<PackageIdentifier>> mSchemasPackageAccessible =
+ new ArrayMap<>();
private boolean mForceOverride = false;
private boolean mBuilt = false;
@@ -145,78 +189,112 @@
}
/**
- * Sets visibility on system UI surfaces for schema types.
+ * Sets visibility on system UI surfaces for the given {@code schemaType}.
*
+ * @param schemaType The schema type to set visibility on.
+ * @param visible Whether the {@code schemaType} will be visible or not.
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
- public Builder setSchemaTypeVisibilityForSystemUi(boolean visible,
- @NonNull String... schemaTypes) {
- Preconditions.checkNotNull(schemaTypes);
- return this.setSchemaTypeVisibilityForSystemUi(visible, Arrays.asList(schemaTypes));
- }
-
- /**
- * Sets visibility on system UI surfaces for schema types.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setSchemaTypeVisibilityForSystemUi(boolean visible,
- @NonNull Collection<String> schemaTypes) {
+ public Builder setSchemaTypeVisibilityForSystemUi(@NonNull String schemaType,
+ boolean visible) {
+ Preconditions.checkNotNull(schemaType);
Preconditions.checkState(!mBuilt, "Builder has already been used");
- Preconditions.checkNotNull(schemaTypes);
+
if (visible) {
- mSchemasNotPlatformSurfaceable.removeAll(schemaTypes);
+ mSchemasNotPlatformSurfaceable.remove(schemaType);
} else {
- mSchemasNotPlatformSurfaceable.addAll(schemaTypes);
+ mSchemasNotPlatformSurfaceable.add(schemaType);
}
return this;
}
/**
- * Sets visibility on system UI surfaces for schema types.
+ * Sets visibility for a package for the given {@code schemaType}.
*
- * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
- * has not generated a schema for the given data classes.
+ * @param schemaType The schema type to set visibility on.
+ * @param visible Whether the {@code schemaType} will be visible or not.
+ * @param packageIdentifier Represents the package that will be granted visibility.
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
- public Builder setDataClassVisibilityForSystemUi(boolean visible,
- @NonNull Class<?>... dataClasses) throws AppSearchException {
- Preconditions.checkNotNull(dataClasses);
- return setDataClassVisibilityForSystemUi(visible, Arrays.asList(dataClasses));
- }
-
- /**
- * Sets visibility on system UI surfaces for schema types.
- *
- * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
- * has not generated a schema for the given data classes.
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setDataClassVisibilityForSystemUi(boolean visible,
- @NonNull Collection<Class<?>> dataClasses) throws AppSearchException {
+ public Builder setSchemaTypeVisibilityForPackage(@NonNull String schemaType,
+ boolean visible, @NonNull PackageIdentifier packageIdentifier) {
+ Preconditions.checkNotNull(schemaType);
+ Preconditions.checkNotNull(packageIdentifier);
Preconditions.checkState(!mBuilt, "Builder has already been used");
- Preconditions.checkNotNull(dataClasses);
- DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
- for (Class<?> dataClass : dataClasses) {
- DataClassFactory<?> factory = registry.getOrCreateFactory(dataClass);
- if (visible) {
- mSchemasNotPlatformSurfaceable.remove(factory.getSchemaType());
- } else {
- mSchemasNotPlatformSurfaceable.add(factory.getSchemaType());
+
+ Set<PackageIdentifier> packageIdentifiers =
+ mSchemasPackageAccessible.get(schemaType);
+ if (visible) {
+ if (packageIdentifiers == null) {
+ packageIdentifiers = new ArraySet<>();
+ }
+ packageIdentifiers.add(packageIdentifier);
+ mSchemasPackageAccessible.put(schemaType, packageIdentifiers);
+ } else {
+ if (packageIdentifiers == null) {
+ // Return early since there was nothing set to begin with.
+ return this;
+ }
+ packageIdentifiers.remove(packageIdentifier);
+ if (packageIdentifiers.isEmpty()) {
+ // Remove the entire key so that we don't have empty sets as values.
+ mSchemasPackageAccessible.remove(schemaType);
}
}
+
return this;
}
/**
+ * Sets visibility on system UI surfaces for the given {@code dataClass}.
+ *
+ * @param dataClass The schema to set visibility on.
+ * @param visible Whether the {@code schemaType} will be visible or not.
+ * @return {@link SetSchemaRequest.Builder}
+ * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
+ * has not generated a schema for the given data classes.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public Builder setDataClassVisibilityForSystemUi(@NonNull Class<?> dataClass,
+ boolean visible) throws AppSearchException {
+ Preconditions.checkNotNull(dataClass);
+
+ DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
+ DataClassFactory<?> factory = registry.getOrCreateFactory(dataClass);
+ return setSchemaTypeVisibilityForSystemUi(factory.getSchemaType(), visible);
+ }
+
+ /**
+ * Sets visibility for a package for the given {@code dataClass}.
+ *
+ * @param dataClass The schema to set visibility on.
+ * @param visible Whether the {@code schemaType} will be visible or not.
+ * @param packageIdentifier Represents the package that will be granted visibility
+ * @return {@link SetSchemaRequest.Builder}
+ * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
+ * has not generated a schema for the given data classes.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public Builder setDataClassVisibilityForPackage(@NonNull Class<?> dataClass,
+ boolean visible, @NonNull PackageIdentifier packageIdentifier)
+ throws AppSearchException {
+ Preconditions.checkNotNull(dataClass);
+
+ DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
+ DataClassFactory<?> factory = registry.getOrCreateFactory(dataClass);
+ return setSchemaTypeVisibilityForPackage(factory.getSchemaType(), visible,
+ packageIdentifier);
+ }
+
+ /**
* Configures the {@link SetSchemaRequest} to delete any existing documents that don't
* follow the new schema.
*
@@ -244,20 +322,23 @@
// Verify that any schema types with visibility settings refer to a real schema.
// Create a copy because we're going to remove from the set for verification purposes.
- Set<String> schemasNotPlatformSurfaceableCopy = new ArraySet<>(
+ Set<String> referencedSchemas = new ArraySet<>(
mSchemasNotPlatformSurfaceable);
+ referencedSchemas.addAll(mSchemasPackageAccessible.keySet());
+
for (AppSearchSchema schema : mSchemas) {
- schemasNotPlatformSurfaceableCopy.remove(schema.getSchemaType());
+ referencedSchemas.remove(schema.getSchemaType());
}
- if (!schemasNotPlatformSurfaceableCopy.isEmpty()) {
+ if (!referencedSchemas.isEmpty()) {
// We still have schema types that weren't seen in our mSchemas set. This means
// there wasn't a corresponding AppSearchSchema.
throw new IllegalArgumentException(
- "Schema types " + schemasNotPlatformSurfaceableCopy
+ "Schema types " + referencedSchemas
+ " referenced, but were not added.");
}
return new SetSchemaRequest(mSchemas, mSchemasNotPlatformSurfaceable,
+ mSchemasPackageAccessible,
mForceOverride);
}
}
diff --git a/appsearch/exportToFramework.py b/appsearch/exportToFramework.py
index 33f9429..1a646e2 100755
--- a/appsearch/exportToFramework.py
+++ b/appsearch/exportToFramework.py
@@ -32,7 +32,7 @@
JETPACK_IMPL_TEST_ROOT = 'local-storage/src/androidTest/java/androidx/appsearch'
# Framework paths relative to frameworks/base/apex/appsearch
-FRAMEWORK_API_ROOT = 'framework/java/android/app/appsearch'
+FRAMEWORK_API_ROOT = 'framework/java/external/android/app/appsearch'
FRAMEWORK_API_TEST_ROOT = (
'../../core/tests/coretests/src/'
'android/app/appsearch/external')
@@ -52,18 +52,10 @@
self._jetpack_appsearch_root = jetpack_appsearch_root
self._framework_appsearch_root = framework_appsearch_root
- def _PruneDir(self, dir_to_prune, allow_list=None):
- all_files = []
+ def _PruneDir(self, dir_to_prune):
for walk_path, walk_folders, walk_files in os.walk(dir_to_prune):
for walk_filename in walk_files:
abs_path = os.path.join(walk_path, walk_filename)
- all_files.append(abs_path)
-
- for abs_path in all_files:
- rel_path = os.path.relpath(abs_path, dir_to_prune)
- if allow_list and rel_path in allow_list:
- print('Prune: skip "%s"' % abs_path)
- else:
print('Prune: remove "%s"' % abs_path)
os.remove(abs_path)
@@ -137,14 +129,7 @@
api_test_dest_dir = os.path.join(self._framework_appsearch_root, FRAMEWORK_API_TEST_ROOT)
# Prune existing files
- self._PruneDir(api_dest_dir, allow_list=[
- 'AppSearchBatchResult.java',
- 'AppSearchManager.java',
- 'AppSearchManagerFrameworkInitializer.java',
- 'AppSearchResult.java',
- 'IAppSearchManager.aidl',
- 'SearchResults.java',
- ])
+ self._PruneDir(api_dest_dir)
self._PruneDir(api_test_dest_dir)
# Copy api classes. We can't use _TransformAndCopyFolder here because we
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
index cad539b..101356e 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
@@ -36,6 +36,7 @@
import com.google.android.icing.proto.SearchSpecProto;
import com.google.android.icing.proto.StringIndexingConfig;
import com.google.android.icing.proto.TermMatchType;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.junit.Before;
@@ -45,9 +46,7 @@
import java.util.ArrayList;
import java.util.Collections;
-import java.util.HashSet;
import java.util.List;
-import java.util.Set;
public class AppSearchImplTest {
@Rule
@@ -279,16 +278,16 @@
@Test
public void testOptimize() throws Exception {
// Insert schema
- Set<AppSearchSchema> schemas =
- Collections.singleton(new AppSearchSchema.Builder("type").build());
+ List<AppSearchSchema> schemas =
+ Collections.singletonList(new AppSearchSchema.Builder("type").build());
mAppSearchImpl.setSchema("database", schemas, /*schemasNotPlatformSurfaceable=*/
- Collections.emptySet(), /*forceOverride=*/ false);
+ Collections.emptyList(), /*forceOverride=*/ false);
// Insert enough documents.
for (int i = 0; i < AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT
+ AppSearchImpl.CHECK_OPTIMIZE_INTERVAL; i++) {
GenericDocument document =
- new GenericDocument.Builder("uri" + i, "type").setNamespace(
+ new GenericDocument.Builder<>("uri" + i, "type").setNamespace(
"namespace").build();
mAppSearchImpl.putDocument("database", document);
}
@@ -327,20 +326,19 @@
SearchSpecProto.newBuilder().setQuery("");
// Insert schema
- Set<AppSearchSchema> schemas =
- Collections.singleton(new AppSearchSchema.Builder("type").build());
+ List<AppSearchSchema> schemas =
+ Collections.singletonList(new AppSearchSchema.Builder("type").build());
mAppSearchImpl.setSchema("database", schemas, /*schemasNotPlatformSurfaceable=*/
- Collections.emptySet(), /*forceOverride=*/ false);
+ Collections.emptyList(), /*forceOverride=*/ false);
// Insert document
- GenericDocument document = new GenericDocument.Builder("uri", "type").setNamespace(
+ GenericDocument document = new GenericDocument.Builder<>("uri", "type").setNamespace(
"namespace").build();
mAppSearchImpl.putDocument("database", document);
// Rewrite SearchSpec
- mAppSearchImpl.rewriteSearchSpecForDatabasesLocked(searchSpecProto,
- Collections.singleton(
- "database"));
+ mAppSearchImpl.rewriteSearchSpecForDatabasesLocked(
+ searchSpecProto, Collections.singleton("database"));
assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly("database/type");
assertThat(searchSpecProto.getNamespaceFiltersList()).containsExactly("database/namespace");
}
@@ -351,20 +349,20 @@
SearchSpecProto.newBuilder().setQuery("");
// Insert schema
- Set<AppSearchSchema> schemas = Set.of(
+ List<AppSearchSchema> schemas = ImmutableList.of(
new AppSearchSchema.Builder("typeA").build(),
new AppSearchSchema.Builder("typeB").build());
mAppSearchImpl.setSchema("database1", schemas, /*schemasNotPlatformSurfaceable=*/
- Collections.emptySet(), /*forceOverride=*/ false);
+ Collections.emptyList(), /*forceOverride=*/ false);
mAppSearchImpl.setSchema("database2", schemas, /*schemasNotPlatformSurfaceable=*/
- Collections.emptySet(), /*forceOverride=*/ false);
+ Collections.emptyList(), /*forceOverride=*/ false);
// Insert documents
- GenericDocument document1 = new GenericDocument.Builder("uri", "typeA").setNamespace(
+ GenericDocument document1 = new GenericDocument.Builder<>("uri", "typeA").setNamespace(
"namespace").build();
mAppSearchImpl.putDocument("database1", document1);
- GenericDocument document2 = new GenericDocument.Builder("uri", "typeB").setNamespace(
+ GenericDocument document2 = new GenericDocument.Builder<>("uri", "typeB").setNamespace(
"namespace").build();
mAppSearchImpl.putDocument("database2", document2);
@@ -417,11 +415,11 @@
@Test
public void testSetSchema() throws Exception {
- Set<AppSearchSchema> schemas =
- Collections.singleton(new AppSearchSchema.Builder("Email").build());
+ List<AppSearchSchema> schemas =
+ Collections.singletonList(new AppSearchSchema.Builder("Email").build());
// Set schema Email to AppSearch database1
mAppSearchImpl.setSchema("database1", schemas, /*schemasNotPlatformSurfaceable=*/
- Collections.emptySet(), /*forceOverride=*/ false);
+ Collections.emptyList(), /*forceOverride=*/ false);
// Create expected schemaType proto.
SchemaProto expectedProto = SchemaProto.newBuilder()
@@ -437,19 +435,22 @@
@Test
public void testSetSchema_existingSchemaRetainsVisibilitySetting() throws Exception {
- mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
+ mAppSearchImpl.setSchema("database", Collections.singletonList(new AppSearchSchema.Builder(
"schema1").build()), /*schemasNotPlatformSurfaceable=*/
- Collections.singleton("schema1"), /*forceOverride=*/ false);
+ Collections.singletonList("schema1"), /*forceOverride=*/ false);
// "schema1" is platform hidden now
assertThat(mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
"database")).containsExactly("database/schema1");
// Add a new schema, and include the already-existing "schema1"
- mAppSearchImpl.setSchema("database", Set.of(new AppSearchSchema.Builder(
- "schema1").build(), new AppSearchSchema.Builder(
- "schema2").build()), /*schemasNotPlatformSurfaceable=*/
- Collections.singleton("schema1"), /*forceOverride=*/ false);
+ mAppSearchImpl.setSchema(
+ "database",
+ ImmutableList.of(
+ new AppSearchSchema.Builder("schema1").build(),
+ new AppSearchSchema.Builder("schema2").build()),
+ /*schemasNotPlatformSurfaceable=*/ Collections.singletonList("schema1"),
+ /*forceOverride=*/ false);
// Check that "schema1" is still platform hidden, but "schema2" is the default platform
// visible.
@@ -459,12 +460,12 @@
@Test
public void testRemoveSchema() throws Exception {
- Set<AppSearchSchema> schemas = new HashSet<>();
- schemas.add(new AppSearchSchema.Builder("Email").build());
- schemas.add(new AppSearchSchema.Builder("Document").build());
+ List<AppSearchSchema> schemas = ImmutableList.of(
+ new AppSearchSchema.Builder("Email").build(),
+ new AppSearchSchema.Builder("Document").build());
// Set schema Email and Document to AppSearch database1
mAppSearchImpl.setSchema("database1", schemas, /*schemasNotPlatformSurfaceable=*/
- Collections.emptySet(), /*forceOverride=*/ false);
+ Collections.emptyList(), /*forceOverride=*/ false);
// Create expected schemaType proto.
SchemaProto expectedProto = SchemaProto.newBuilder()
@@ -479,19 +480,20 @@
assertThat(mAppSearchImpl.getSchemaProtoLocked().getTypesList())
.containsExactlyElementsIn(expectedTypes);
- final Set<AppSearchSchema> finalSchemas = Collections.singleton(new AppSearchSchema.Builder(
- "Email").build());
+ final List<AppSearchSchema> finalSchemas = Collections.singletonList(
+ new AppSearchSchema.Builder(
+ "Email").build());
// Check the incompatible error has been thrown.
AppSearchException e = assertThrows(AppSearchException.class, () ->
mAppSearchImpl.setSchema("database1",
finalSchemas, /*schemasNotPlatformSurfaceable=*/
- Collections.emptySet(), /*forceOverride=*/ false));
+ Collections.emptyList(), /*forceOverride=*/ false));
assertThat(e).hasMessageThat().contains("Schema is incompatible");
assertThat(e).hasMessageThat().contains("Deleted types: [database1/Document]");
// ForceOverride to delete.
mAppSearchImpl.setSchema("database1", finalSchemas, /*schemasNotPlatformSurfaceable=*/
- Collections.emptySet(), /*forceOverride=*/ true);
+ Collections.emptyList(), /*forceOverride=*/ true);
// Check Document schema is removed.
expectedProto = SchemaProto.newBuilder()
@@ -508,15 +510,15 @@
@Test
public void testRemoveSchema_differentDataBase() throws Exception {
// Create schemas
- Set<AppSearchSchema> schemas = new HashSet<>();
- schemas.add(new AppSearchSchema.Builder("Email").build());
- schemas.add(new AppSearchSchema.Builder("Document").build());
+ List<AppSearchSchema> schemas = ImmutableList.of(
+ new AppSearchSchema.Builder("Email").build(),
+ new AppSearchSchema.Builder("Document").build());
// Set schema Email and Document to AppSearch database1 and 2
mAppSearchImpl.setSchema("database1", schemas, /*schemasNotPlatformSurfaceable=*/
- Collections.emptySet(), /*forceOverride=*/ false);
+ Collections.emptyList(), /*forceOverride=*/ false);
mAppSearchImpl.setSchema("database2", schemas, /*schemasNotPlatformSurfaceable=*/
- Collections.emptySet(), /*forceOverride=*/ false);
+ Collections.emptyList(), /*forceOverride=*/ false);
// Create expected schemaType proto.
SchemaProto expectedProto = SchemaProto.newBuilder()
@@ -534,9 +536,9 @@
.containsExactlyElementsIn(expectedTypes);
// Save only Email to database1 this time.
- schemas = Collections.singleton(new AppSearchSchema.Builder("Email").build());
+ schemas = Collections.singletonList(new AppSearchSchema.Builder("Email").build());
mAppSearchImpl.setSchema("database1", schemas, /*schemasNotPlatformSurfaceable=*/
- Collections.emptySet(), /*forceOverride=*/ true);
+ Collections.emptyList(), /*forceOverride=*/ true);
// Create expected schemaType list, database 1 should only contain Email but database 2
// remains in same.
@@ -557,9 +559,9 @@
@Test
public void testRemoveSchema_removedFromVisibilityStore() throws Exception {
- mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
+ mAppSearchImpl.setSchema("database", Collections.singletonList(new AppSearchSchema.Builder(
"schema1").build()), /*schemasNotPlatformSurfaceable=*/
- Collections.singleton("schema1"), /*forceOverride=*/ false);
+ Collections.singletonList("schema1"), /*forceOverride=*/ false);
// "schema1" is platform hidden now
assertThat(mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
@@ -567,8 +569,8 @@
// Remove "schema1" by force overriding
mAppSearchImpl.setSchema("database",
- Collections.emptySet(), /*schemasNotPlatformSurfaceable=*/
- Collections.emptySet(), /*forceOverride=*/ true);
+ Collections.emptyList(), /*schemasNotPlatformSurfaceable=*/
+ Collections.emptyList(), /*forceOverride=*/ true);
// Check that "schema1" is no longer considered platform hidden
assertThat(
@@ -577,9 +579,9 @@
// Add "schema1" back, it gets default visibility settings which means it's not platform
// hidden.
- mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
+ mAppSearchImpl.setSchema("database", Collections.singletonList(new AppSearchSchema.Builder(
"schema1").build()), /*schemasNotPlatformSurfaceable=*/
- Collections.emptySet(), /*forceOverride=*/ false);
+ Collections.emptyList(), /*forceOverride=*/ false);
assertThat(
mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
"database")).isEmpty();
@@ -587,9 +589,9 @@
@Test
public void testSetSchema_defaultPlatformVisible() throws Exception {
- mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
+ mAppSearchImpl.setSchema("database", Collections.singletonList(new AppSearchSchema.Builder(
"Schema").build()), /*schemasNotPlatformSurfaceable=*/
- Collections.emptySet(), /*forceOverride=*/ false);
+ Collections.emptyList(), /*forceOverride=*/ false);
assertThat(
mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
"database")).isEmpty();
@@ -597,9 +599,9 @@
@Test
public void testSetSchema_platformHidden() throws Exception {
- mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
+ mAppSearchImpl.setSchema("database", Collections.singletonList(new AppSearchSchema.Builder(
"Schema").build()), /*schemasNotPlatformSurfaceable=*/
- Collections.singleton("Schema"), /*forceOverride=*/ false);
+ Collections.singletonList("Schema"), /*forceOverride=*/ false);
assertThat(mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
"database")).containsExactly("database/Schema");
}
@@ -609,9 +611,9 @@
// Nothing exists yet
assertThat(mAppSearchImpl.hasSchemaTypeLocked("database", "Schema")).isFalse();
- mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
+ mAppSearchImpl.setSchema("database", Collections.singletonList(new AppSearchSchema.Builder(
"Schema").build()), /*schemasNotPlatformSurfaceable=*/
- Collections.emptySet(), /*forceOverride=*/ false);
+ Collections.emptyList(), /*forceOverride=*/ false);
assertThat(mAppSearchImpl.hasSchemaTypeLocked("database", "Schema")).isTrue();
assertThat(mAppSearchImpl.hasSchemaTypeLocked("database", "UnknownSchema")).isFalse();
@@ -624,16 +626,16 @@
VisibilityStore.DATABASE_NAME);
// Has database1
- mAppSearchImpl.setSchema("database1", Collections.singleton(new AppSearchSchema.Builder(
+ mAppSearchImpl.setSchema("database1", Collections.singletonList(new AppSearchSchema.Builder(
"schema").build()), /*schemasNotPlatformSurfaceable=*/
- Collections.emptySet(), /*forceOverride=*/ false);
+ Collections.emptyList(), /*forceOverride=*/ false);
assertThat(mAppSearchImpl.getDatabasesLocked()).containsExactly(
VisibilityStore.DATABASE_NAME, "database1");
// Has both databases
- mAppSearchImpl.setSchema("database2", Collections.singleton(new AppSearchSchema.Builder(
+ mAppSearchImpl.setSchema("database2", Collections.singletonList(new AppSearchSchema.Builder(
"schema").build()), /*schemasNotPlatformSurfaceable=*/
- Collections.emptySet(), /*forceOverride=*/ false);
+ Collections.emptyList(), /*forceOverride=*/ false);
assertThat(mAppSearchImpl.getDatabasesLocked()).containsExactly(
VisibilityStore.DATABASE_NAME, "database1", "database2");
}
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java
index d4a30f4..f3732a1 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java
@@ -18,13 +18,14 @@
import static com.google.common.truth.Truth.assertThat;
+import com.google.common.collect.ImmutableSet;
+
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.util.Collections;
-import java.util.Set;
public class VisibilityStoreTest {
@@ -41,20 +42,28 @@
@Test
public void testSetVisibility() throws Exception {
- mVisibilityStore.setVisibility(
- "database", /*schemasNotPlatformSurfaceable=*/ Set.of("schema1", "schema2"));
+ mVisibilityStore.setVisibility("database",
+ /*schemasNotPlatformSurfaceable=*/ ImmutableSet.of("schema1", "schema2"));
assertThat(mVisibilityStore.getSchemasNotPlatformSurfaceable("database"))
- .containsExactly("schema1", "schema2");
+ .containsExactlyElementsIn(ImmutableSet.of("schema1", "schema2"));
// New .setVisibility() call completely overrides previous visibility settings. So
- // "schema1" isn't preserved.
- mVisibilityStore.setVisibility(
- "database", /*schemasNotPlatformSurfaceable=*/ Set.of("schema1", "schema3"));
+ // "schema2" isn't preserved.
+ mVisibilityStore.setVisibility("database",
+ /*schemasNotPlatformSurfaceable=*/ ImmutableSet.of("schema1", "schema3"));
assertThat(mVisibilityStore.getSchemasNotPlatformSurfaceable("database"))
- .containsExactly("schema1", "schema3");
+ .containsExactlyElementsIn(ImmutableSet.of("schema1", "schema3"));
mVisibilityStore.setVisibility(
"database", /*schemasNotPlatformSurfaceable=*/ Collections.emptySet());
assertThat(mVisibilityStore.getSchemasNotPlatformSurfaceable("database")).isEmpty();
}
+
+ @Test
+ public void testEmptyDatabase() throws Exception {
+ mVisibilityStore.setVisibility(LocalStorage.DEFAULT_DATABASE_NAME,
+ /*schemasNotPlatformSurfaceable=*/ ImmutableSet.of("schema1", "schema2"));
+ assertThat(mVisibilityStore.getSchemasNotPlatformSurfaceable(""))
+ .containsExactlyElementsIn(ImmutableSet.of("schema1", "schema2"));
+ }
}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index 2ec2d82..9c25f4b 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -61,6 +61,7 @@
import com.google.android.icing.proto.StatusProto;
import java.io.File;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -232,17 +233,19 @@
* which do not comply with the new schema will be deleted.
* @throws AppSearchException on IcingSearchEngine error.
*/
- public void setSchema(@NonNull String databaseName, @NonNull Set<AppSearchSchema> schemas,
- @NonNull Set<String> schemasNotPlatformSurfaceable,
+ public void setSchema(
+ @NonNull String databaseName,
+ @NonNull List<AppSearchSchema> schemas,
+ @NonNull List<String> schemasNotPlatformSurfaceable,
boolean forceOverride) throws AppSearchException {
mReadWriteLock.writeLock().lock();
try {
SchemaProto.Builder existingSchemaBuilder = getSchemaProtoLocked().toBuilder();
SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder();
- for (AppSearchSchema schema : schemas) {
+ for (int i = 0; i < schemas.size(); i++) {
SchemaTypeConfigProto schemaTypeProto =
- SchemaToProtoConverter.toSchemaTypeConfigProto(schema);
+ SchemaToProtoConverter.toSchemaTypeConfigProto(schemas.get(i));
newSchemaBuilder.addTypes(schemaTypeProto);
}
@@ -281,8 +284,9 @@
String databasePrefix = getDatabasePrefix(databaseName);
Set<String> qualifiedSchemasNotPlatformSurfaceable =
new ArraySet<>(schemasNotPlatformSurfaceable.size());
- for (String schema : schemasNotPlatformSurfaceable) {
- qualifiedSchemasNotPlatformSurfaceable.add(databasePrefix + schema);
+ for (int i = 0; i < schemasNotPlatformSurfaceable.size(); i++) {
+ qualifiedSchemasNotPlatformSurfaceable.add(
+ databasePrefix + schemasNotPlatformSurfaceable.get(i));
}
mVisibilityStoreLocked.setVisibility(databaseName,
qualifiedSchemasNotPlatformSurfaceable);
@@ -306,11 +310,11 @@
*
* <p>This method belongs to query group.
*
- * @param databaseName The name of the database where this schema lives.
+ * @param databaseName The name of the database where this schema lives.
* @throws AppSearchException on IcingSearchEngine error.
*/
@NonNull
- public Set<AppSearchSchema> getSchema(@NonNull String databaseName) throws AppSearchException {
+ public List<AppSearchSchema> getSchema(@NonNull String databaseName) throws AppSearchException {
SchemaProto fullSchema;
mReadWriteLock.readLock().lock();
try {
@@ -319,7 +323,7 @@
mReadWriteLock.readLock().unlock();
}
- Set<AppSearchSchema> result = new ArraySet<>();
+ List<AppSearchSchema> result = new ArrayList<>();
for (int i = 0; i < fullSchema.getTypesCount(); i++) {
String typeDatabase = getDatabaseName(fullSchema.getTypes(i).getSchemaType());
if (!databaseName.equals(typeDatabase)) {
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
index 7d87f11..3426cfe 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/LocalStorage.java
@@ -23,6 +23,7 @@
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.appsearch.app.AppSearchResult;
import androidx.appsearch.app.AppSearchSession;
@@ -50,8 +51,13 @@
* delete, etc..).
*/
public class LocalStorage {
- /** The default empty database name.*/
- private static final String DEFAULT_DATABASE_NAME = "";
+ /**
+ * The default empty database name.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @VisibleForTesting
+ public static final String DEFAULT_DATABASE_NAME = "";
private static volatile ListenableFuture<AppSearchResult<LocalStorage>> sInstance;
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
index 12b25fa..f9440ec 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
@@ -31,10 +31,13 @@
import androidx.appsearch.app.SearchSpec;
import androidx.appsearch.app.SetSchemaRequest;
import androidx.appsearch.localstorage.util.FutureUtil;
+import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
import com.google.common.util.concurrent.ListenableFuture;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
@@ -67,8 +70,10 @@
return execute(() -> {
try {
mAppSearchImpl.setSchema(
- mDatabaseName, request.getSchemas(),
- request.getSchemasNotPlatformSurfaceable(), request.isForceOverride());
+ mDatabaseName,
+ new ArrayList<>(request.getSchemas()),
+ new ArrayList<>(request.getSchemasNotPlatformSurfaceable()),
+ request.isForceOverride());
return AppSearchResult.newSuccessfulResult(/*value=*/ null);
} catch (Throwable t) {
return throwableToFailedResult(t);
@@ -81,7 +86,8 @@
public ListenableFuture<AppSearchResult<Set<AppSearchSchema>>> getSchema() {
return execute(() -> {
try {
- return AppSearchResult.newSuccessfulResult(mAppSearchImpl.getSchema(mDatabaseName));
+ List<AppSearchSchema> schemas = mAppSearchImpl.getSchema(mDatabaseName);
+ return AppSearchResult.newSuccessfulResult(new ArraySet<>(schemas));
} catch (Throwable t) {
return throwableToFailedResult(t);
}
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java
index a95231f..935662e 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java
@@ -65,7 +65,11 @@
static final String DATABASE_NAME = "$$__AppSearch__Database";
// Namespace of documents that contain visibility settings
- private static final String NAMESPACE = "namespace";
+ private static final String NAMESPACE = GenericDocument.DEFAULT_NAMESPACE;
+
+ // Prefix to add to all visibility document uri's. IcingSearchEngine doesn't allow empty uri's.
+ private static final String URI_PREFIX = "uri:";
+
private final AppSearchImpl mAppSearchImpl;
// The map contains schemas that are platform-hidden for each database. All schemas in the map
@@ -97,7 +101,7 @@
if (!mAppSearchImpl.hasSchemaTypeLocked(DATABASE_NAME, SCHEMA_TYPE)) {
// Schema type doesn't exist yet. Add it.
mAppSearchImpl.setSchema(DATABASE_NAME,
- Collections.singleton(new AppSearchSchema.Builder(SCHEMA_TYPE)
+ Collections.singletonList(new AppSearchSchema.Builder(SCHEMA_TYPE)
.addProperty(new AppSearchSchema.PropertyConfig.Builder(
NOT_PLATFORM_SURFACEABLE_PROPERTY)
.setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
@@ -105,7 +109,7 @@
AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
.build())
.build()),
- /*schemasNotPlatformSurfaceable=*/ Collections.emptySet(),
+ /*schemasNotPlatformSurfaceable=*/ Collections.emptyList(),
/*forceOverride=*/ false);
}
@@ -119,11 +123,12 @@
try {
// Note: We use the other clients' database names as uris
GenericDocument document = mAppSearchImpl.getDocument(
- DATABASE_NAME, NAMESPACE, /*uri=*/ database);
+ DATABASE_NAME, NAMESPACE, /*uri=*/ addUriPrefix(database));
String[] schemas = document.getPropertyStringArray(
NOT_PLATFORM_SURFACEABLE_PROPERTY);
- mNotPlatformSurfaceableMap.put(database, new ArraySet<>(Arrays.asList(schemas)));
+ mNotPlatformSurfaceableMap.put(database,
+ new ArraySet<>(Arrays.asList(schemas)));
} catch (AppSearchException e) {
if (e.getResultCode() == AppSearchResult.RESULT_NOT_FOUND) {
// TODO(b/172068212): This indicates some desync error. We were expecting a
@@ -155,7 +160,7 @@
// Persist the document
GenericDocument.Builder visibilityDocument = new GenericDocument.Builder(
- /*uri=*/ databaseName, SCHEMA_TYPE)
+ /*uri=*/ addUriPrefix(databaseName), SCHEMA_TYPE)
.setNamespace(NAMESPACE);
if (!schemasNotPlatformSurfaceable.isEmpty()) {
visibilityDocument.setPropertyString(NOT_PLATFORM_SURFACEABLE_PROPERTY,
@@ -195,4 +200,14 @@
mNotPlatformSurfaceableMap.clear();
initialize();
}
+
+ /**
+ * Adds a uri prefix to create a visibility store document's uri.
+ *
+ * @param uri Non-prefixed uri
+ * @return Prefixed uri
+ */
+ private static String addUriPrefix(String uri) {
+ return URI_PREFIX + uri;
+ }
}
diff --git a/benchmark/common/api/current.txt b/benchmark/common/api/current.txt
index c023743..167ebf4 100644
--- a/benchmark/common/api/current.txt
+++ b/benchmark/common/api/current.txt
@@ -4,6 +4,9 @@
public final class ArgumentsKt {
}
+ public final class BenchmarkResultKt {
+ }
+
public final class BenchmarkState {
method public boolean keepRunning();
method public void pauseTiming();
diff --git a/benchmark/common/api/public_plus_experimental_current.txt b/benchmark/common/api/public_plus_experimental_current.txt
index dc2f41d..e54d1d6 100644
--- a/benchmark/common/api/public_plus_experimental_current.txt
+++ b/benchmark/common/api/public_plus_experimental_current.txt
@@ -4,6 +4,34 @@
public final class ArgumentsKt {
}
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final class BenchmarkResult {
+ ctor public BenchmarkResult(String className, String testName, long totalRunTimeNs, java.util.List<androidx.benchmark.MetricResult> metrics, int repeatIterations, long thermalThrottleSleepSeconds, int warmupIterations);
+ method public String component1();
+ method public String component2();
+ method public long component3();
+ method public java.util.List<androidx.benchmark.MetricResult> component4();
+ method public int component5();
+ method public long component6();
+ method public int component7();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public androidx.benchmark.BenchmarkResult copy(String className, String testName, long totalRunTimeNs, java.util.List<androidx.benchmark.MetricResult> metrics, int repeatIterations, long thermalThrottleSleepSeconds, int warmupIterations);
+ method public String getClassName();
+ method public java.util.List<androidx.benchmark.MetricResult> getMetrics();
+ method public int getRepeatIterations();
+ method public androidx.benchmark.Stats getStats(String which);
+ method public String getTestName();
+ method public int getWarmupIterations();
+ property public final String className;
+ property public final java.util.List<androidx.benchmark.MetricResult> metrics;
+ property public final int repeatIterations;
+ property public final String testName;
+ property public final int warmupIterations;
+ field public final long thermalThrottleSleepSeconds;
+ field public final long totalRunTimeNs;
+ }
+
+ public final class BenchmarkResultKt {
+ }
+
public final class BenchmarkState {
ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public BenchmarkState();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public long getMinTimeNanos();
@@ -40,9 +68,26 @@
public final class MetricNameUtilsKt {
}
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final class MetricResult {
+ ctor public MetricResult(java.util.List<java.lang.Long> data, androidx.benchmark.Stats stats);
+ ctor public MetricResult(String name, long[] data);
+ method public java.util.List<java.lang.Long> component1();
+ method public androidx.benchmark.Stats component2();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public androidx.benchmark.MetricResult copy(java.util.List<java.lang.Long> data, androidx.benchmark.Stats stats);
+ method public java.util.List<java.lang.Long> getData();
+ method public androidx.benchmark.Stats getStats();
+ property public final java.util.List<java.lang.Long> data;
+ property public final androidx.benchmark.Stats stats;
+ }
+
public final class ProfilerKt {
}
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final class ResultWriter {
+ method public void appendReport(androidx.benchmark.BenchmarkResult benchmarkResult);
+ field public static final androidx.benchmark.ResultWriter INSTANCE;
+ }
+
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final class Stats {
ctor public Stats(long[] data, String name);
method public long getMax();
diff --git a/benchmark/common/api/restricted_current.txt b/benchmark/common/api/restricted_current.txt
index 84aef03..b59d142 100644
--- a/benchmark/common/api/restricted_current.txt
+++ b/benchmark/common/api/restricted_current.txt
@@ -4,6 +4,9 @@
public final class ArgumentsKt {
}
+ public final class BenchmarkResultKt {
+ }
+
public final class BenchmarkState {
method public boolean keepRunning();
method @kotlin.PublishedApi internal boolean keepRunningInternal();
diff --git a/benchmark/common/src/androidTest/java/androidx/benchmark/BenchmarkStateTest.kt b/benchmark/common/src/androidTest/java/androidx/benchmark/BenchmarkStateTest.kt
index f65209e..ef9b83d 100644
--- a/benchmark/common/src/androidTest/java/androidx/benchmark/BenchmarkStateTest.kt
+++ b/benchmark/common/src/androidTest/java/androidx/benchmark/BenchmarkStateTest.kt
@@ -262,12 +262,16 @@
thermalThrottleSleepSeconds = 0,
repeatIterations = 1
)
- val expectedReport = BenchmarkState.Report(
+ val expectedReport = BenchmarkResult(
className = "className",
testName = "testName",
totalRunTimeNs = 900000000,
- data = listOf(listOf(100L, 200L, 300L)),
- stats = listOf(Stats(longArrayOf(100, 200, 300), "timeNs")),
+ metrics = listOf(
+ MetricResult(
+ name = "timeNs",
+ data = longArrayOf(100, 200, 300)
+ )
+ ),
repeatIterations = 1,
thermalThrottleSleepSeconds = 0,
warmupIterations = 1
diff --git a/benchmark/common/src/androidTest/java/androidx/benchmark/ResultWriterTest.kt b/benchmark/common/src/androidTest/java/androidx/benchmark/ResultWriterTest.kt
index cc7513a..cae9a18 100644
--- a/benchmark/common/src/androidTest/java/androidx/benchmark/ResultWriterTest.kt
+++ b/benchmark/common/src/androidTest/java/androidx/benchmark/ResultWriterTest.kt
@@ -28,30 +28,31 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
-class ResultWriterTest {
-
+public class ResultWriterTest {
@get:Rule
val tempFolder = TemporaryFolder()
- private val data = arrayOf(longArrayOf(100, 101, 102))
- private val names = listOf("timeNs")
+ private val metricResults = listOf(
+ MetricResult(
+ name = "timeNs",
+ data = longArrayOf(100L, 101L, 102L)
+ )
+ )
- private val reportA = BenchmarkState.Report(
+ private val reportA = BenchmarkResult(
testName = "MethodA",
className = "package.Class1",
totalRunTimeNs = 900000000,
- data = data.map { it.toList() },
- stats = data.mapIndexed { i, it -> Stats(it, names[i]) },
+ metrics = metricResults,
repeatIterations = 100000,
thermalThrottleSleepSeconds = 90000000,
warmupIterations = 8000
)
- private val reportB = BenchmarkState.Report(
+ private val reportB = BenchmarkResult(
testName = "MethodB",
className = "package.Class2",
totalRunTimeNs = 900000000,
- data = data.map { it.toList() },
- stats = data.mapIndexed { i, it -> Stats(it, names[i]) },
+ metrics = metricResults,
repeatIterations = 100000,
thermalThrottleSleepSeconds = 90000000,
warmupIterations = 8000
@@ -145,12 +146,11 @@
@Test
fun validateJsonWithParams() {
- val reportWithParams = BenchmarkState.Report(
+ val reportWithParams = BenchmarkResult(
testName = "MethodWithParams[number=2,primeNumber=true]",
className = "package.Class",
totalRunTimeNs = 900000000,
- data = data.map { it.toList() },
- stats = data.mapIndexed { i, it -> Stats(it, names[i]) },
+ metrics = metricResults,
repeatIterations = 100000,
thermalThrottleSleepSeconds = 90000000,
warmupIterations = 8000
@@ -175,12 +175,11 @@
@Test
fun validateJsonWithInvalidParams() {
- val reportWithInvalidParams = BenchmarkState.Report(
+ val reportWithInvalidParams = BenchmarkResult(
testName = "MethodWithParams[number=2,=true,]",
className = "package.Class",
totalRunTimeNs = 900000000,
- data = data.map { it.toList() },
- stats = data.mapIndexed { i, it -> Stats(it, names[i]) },
+ metrics = metricResults,
repeatIterations = 100000,
thermalThrottleSleepSeconds = 90000000,
warmupIterations = 8000
diff --git a/benchmark/common/src/main/java/androidx/benchmark/BenchmarkResult.kt b/benchmark/common/src/main/java/androidx/benchmark/BenchmarkResult.kt
new file mode 100644
index 0000000..7129b3c
--- /dev/null
+++ b/benchmark/common/src/main/java/androidx/benchmark/BenchmarkResult.kt
@@ -0,0 +1,63 @@
+/*
+ * 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
+
+import androidx.annotation.RestrictTo
+
+/**
+ * Data for a single metric from a single benchmark test method run.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public data class MetricResult(
+ val data: List<Long>,
+ val stats: Stats
+) {
+ public constructor(
+ name: String,
+ data: LongArray
+ ) : this(data.toList(), Stats(data, name))
+}
+
+/**
+ * Data capture from a single benchmark test method run.
+ *
+ * Each field directly corresponds to JSON output, though not every JSON object may be
+ * represented directly here.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public data class BenchmarkResult(
+ val className: String,
+ val testName: String,
+ @JvmField // Suppress API lint (using JvmField instead of @Suppress to workaround b/175063229))
+ val totalRunTimeNs: Long,
+ val metrics: List<MetricResult>,
+ val repeatIterations: Int,
+ @JvmField // Suppress API lint (using JvmField instead of @Suppress to workaround b/175063229))
+ val thermalThrottleSleepSeconds: Long,
+ val warmupIterations: Int
+) {
+ public fun getStats(which: String): Stats {
+ return metrics.first { it.stats.name == which }.stats
+ }
+}
+
+internal fun metricResultList(stats: List<Stats>, data: List<LongArray>): List<MetricResult> {
+ require(stats.size == data.size)
+ return stats.mapIndexed { index, currentStats ->
+ MetricResult(data[index].toList(), currentStats)
+ }
+}
\ No newline at end of file
diff --git a/benchmark/common/src/main/java/androidx/benchmark/BenchmarkState.kt b/benchmark/common/src/main/java/androidx/benchmark/BenchmarkState.kt
index c74b069..6b66d0a 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/BenchmarkState.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/BenchmarkState.kt
@@ -445,27 +445,11 @@
" Call BenchmarkState.resumeTiming() before BenchmarkState.keepRunning()."
}
- internal data class Report(
- val className: String,
- val testName: String,
- val totalRunTimeNs: Long,
- val data: List<List<Long>>,
- val stats: List<Stats>,
- val repeatIterations: Int,
- val thermalThrottleSleepSeconds: Long,
- val warmupIterations: Int
- ) {
- fun getStats(which: String): Stats {
- return stats.first { it.name == which }
- }
- }
-
- private fun getReport(testName: String, className: String) = Report(
+ private fun getReport(testName: String, className: String) = BenchmarkResult(
className = className,
testName = testName,
totalRunTimeNs = totalRunTimeNs,
- data = allData.map { it.toList() },
- stats = stats,
+ metrics = metricResultList(stats, allData),
repeatIterations = iterationsPerRepeat,
thermalThrottleSleepSeconds = thermalThrottleSleepSeconds,
warmupIterations = warmupRepeats
@@ -609,12 +593,14 @@
) {
val metricsContainer = MetricsContainer(REPEAT_COUNT = dataNs.size)
metricsContainer.data[metricsContainer.data.lastIndex] = dataNs.toLongArray()
- val report = Report(
+ val report = BenchmarkResult(
className = className,
testName = testName,
totalRunTimeNs = totalRunTimeNs,
- data = metricsContainer.data.map { it.toList() },
- stats = metricsContainer.captureFinished(maxIterations = 1),
+ metrics = metricResultList(
+ stats = metricsContainer.captureFinished(maxIterations = 1),
+ data = metricsContainer.data.toList()
+ ),
repeatIterations = repeatIterations,
thermalThrottleSleepSeconds = thermalThrottleSleepSeconds,
warmupIterations = warmupIterations
diff --git a/benchmark/common/src/main/java/androidx/benchmark/ResultWriter.kt b/benchmark/common/src/main/java/androidx/benchmark/ResultWriter.kt
index c62a72fd..7f0219c 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/ResultWriter.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/ResultWriter.kt
@@ -19,17 +19,20 @@
import android.os.Build
import android.util.JsonWriter
import android.util.Log
+import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.test.platform.app.InstrumentationRegistry
import java.io.File
import java.io.IOException
-internal object ResultWriter {
- @VisibleForTesting
- internal val reports = ArrayList<BenchmarkState.Report>()
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public object ResultWriter {
- fun appendReport(report: BenchmarkState.Report) {
- reports.add(report)
+ @VisibleForTesting
+ internal val reports = ArrayList<BenchmarkResult>()
+
+ public fun appendReport(benchmarkResult: BenchmarkResult) {
+ reports.add(benchmarkResult)
if (Arguments.outputEnable) {
// Currently, we just overwrite the whole file
@@ -59,7 +62,7 @@
}
@VisibleForTesting
- internal fun writeReport(file: File, reports: List<BenchmarkState.Report>) {
+ internal fun writeReport(file: File, benchmarkResults: List<BenchmarkResult>) {
file.run {
if (!exists()) {
parentFile?.mkdirs()
@@ -97,7 +100,7 @@
writer.endObject()
writer.name("benchmarks").beginArray()
- reports.forEach { writer.reportObject(it) }
+ benchmarkResults.forEach { writer.reportObject(it) }
writer.endArray()
writer.endObject()
@@ -116,16 +119,16 @@
return endObject()
}
- private fun JsonWriter.reportObject(report: BenchmarkState.Report): JsonWriter {
+ private fun JsonWriter.reportObject(benchmarkResult: BenchmarkResult): JsonWriter {
beginObject()
- .name("name").value(report.testName)
- .name("params").paramsObject(report)
- .name("className").value(report.className)
- .name("totalRunTimeNs").value(report.totalRunTimeNs)
- .name("metrics").metricsContainerObject(report.stats, report.data)
- .name("warmupIterations").value(report.warmupIterations)
- .name("repeatIterations").value(report.repeatIterations)
- .name("thermalThrottleSleepSeconds").value(report.thermalThrottleSleepSeconds)
+ .name("name").value(benchmarkResult.testName)
+ .name("params").paramsObject(benchmarkResult)
+ .name("className").value(benchmarkResult.className)
+ .name("totalRunTimeNs").value(benchmarkResult.totalRunTimeNs)
+ .name("metrics").metricsContainerObject(benchmarkResult.metrics)
+ .name("warmupIterations").value(benchmarkResult.warmupIterations)
+ .name("repeatIterations").value(benchmarkResult.repeatIterations)
+ .name("thermalThrottleSleepSeconds").value(benchmarkResult.thermalThrottleSleepSeconds)
return endObject()
}
@@ -139,24 +142,23 @@
}
private fun JsonWriter.metricsContainerObject(
- stats: List<Stats>,
- data: List<List<Long>>
+ metricResults: List<MetricResult>
): JsonWriter {
beginObject()
- for (i in 0..stats.lastIndex) {
- name(stats[i].name).beginObject()
- statsObject(stats[i])
+ metricResults.forEach {
+ name(it.stats.name).beginObject()
+ statsObject(it.stats)
name("runs").beginArray()
- data[i].forEach { value(it) }
+ it.data.forEach { value(it) }
endArray()
endObject()
}
return endObject()
}
- private fun JsonWriter.paramsObject(report: BenchmarkState.Report): JsonWriter {
+ private fun JsonWriter.paramsObject(benchmarkResult: BenchmarkResult): JsonWriter {
beginObject()
- getParams(report.testName).forEach { name(it.key).value(it.value) }
+ getParams(benchmarkResult.testName).forEach { name(it.key).value(it.value) }
return endObject()
}
diff --git a/benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh b/benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh
index df929f4..b33734b 100755
--- a/benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh
+++ b/benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh
@@ -94,6 +94,19 @@
fi
}
+# Disable CPU hotpluging by killing mpdecision service via ctl.stop system property.
+# This helper checks the state and existence of the mpdecision service via init.svc.
+# Possible values from init.svc: "stopped", "stopping", "running", "restarting"
+function_stop_mpdecision() {
+ MPDECISION_STATUS=`getprop init.svc.mpdecision`
+ while [ "$MPDECISION_STATUS" == "running" ] || [ "$MPDECISION_STATUS" == "restarting" ]; do
+ setprop ctl.stop mpdecision
+ # Give initrc some time to kill the mpdecision service.
+ sleep 0.1
+ MPDECISION_STATUS=`getprop init.svc.mpdecision`
+ done
+}
+
# Find the min or max (little vs big) of CPU max frequency, and lock cores of the selected type to
# an available frequency that's >= $CPU_TARGET_FREQ_PERCENT% of max. Disable other cores.
function_lock_cpu() {
@@ -111,10 +124,14 @@
disableIndices=''
cpu=0
+ # Stop mpdecision (CPU hotplug service) if it exists. Not available on all devices.
+ function_stop_mpdecision
+
# Loop through all available cores; We have to check by the parent folder
# "cpu#" instead of cpu#/online or cpu#/cpufreq directly, since they may
# not be accessible yet.
while [ -d ${CPU_BASE}/cpu${cpu} ]; do
+
# Try to enable core, so we can find its frequencies.
# Note: In cases where the online file is inaccessible, it represents a
# core which cannot be turned off, so we simply assume it is enabled if
diff --git a/benchmark/integration-tests/macrobenchmark-target/build.gradle b/benchmark/integration-tests/macrobenchmark-target/build.gradle
index 1e72999..4de1d5d 100644
--- a/benchmark/integration-tests/macrobenchmark-target/build.gradle
+++ b/benchmark/integration-tests/macrobenchmark-target/build.gradle
@@ -22,8 +22,18 @@
id("kotlin-android")
}
+android {
+ buildTypes {
+ release {
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
+ }
+ }
+}
+
dependencies {
- api(KOTLIN_STDLIB)
+ implementation(KOTLIN_STDLIB)
implementation(CONSTRAINT_LAYOUT, { transitive = true })
implementation("androidx.arch.core:core-runtime:2.1.0")
implementation("androidx.appcompat:appcompat:1.2.0")
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/TrivialStartupActivity.kt b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/TrivialStartupActivity.kt
index 5cf2081..52792d1 100644
--- a/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/TrivialStartupActivity.kt
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/TrivialStartupActivity.kt
@@ -16,11 +16,8 @@
package androidx.benchmark.integration.macrobenchmark.target
-import android.app.Activity
-import android.os.Build
import android.os.Bundle
import android.widget.TextView
-import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
class TrivialStartupActivity : AppCompatActivity() {
@@ -30,18 +27,5 @@
val notice = findViewById<TextView>(R.id.txtNotice)
notice.setText(R.string.app_notice)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- KitkatActivityCompat.reportFullyDrawn(this)
- }
- }
-}
-
-/**
- * Wrapper avoids UnsafeApiCall lint
- */
-@RequiresApi(19)
-object KitkatActivityCompat {
- fun reportFullyDrawn(activity: Activity) {
- activity.reportFullyDrawn()
}
}
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_row.xml b/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_row.xml
index fa8493f..7e77ae3 100644
--- a/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_row.xml
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_row.xml
@@ -13,46 +13,32 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
+<androidx.cardview.widget.CardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:minHeight="64dp"
- android:padding="8dp">
-
- <androidx.cardview.widget.CardView
- android:id="@+id/card"
+ android:layout_margin="8dp">
+ <androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:padding="8dp">
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
- <androidx.constraintlayout.widget.ConstraintLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/content"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ tools:text="Sample text" />
+ <Space
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
- <androidx.appcompat.widget.AppCompatCheckBox
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="end"
- android:layout_marginEnd="16dp"
- android:layout_marginRight="16dp"
- android:layout_marginTop="8dp"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
-
- <androidx.appcompat.widget.AppCompatTextView
- android:id="@+id/content"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginLeft="16dp"
- android:layout_marginStart="16dp"
- android:layout_marginTop="8dp"
- android:padding="8dp"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- tools:text="Sample text" />
- </androidx.constraintlayout.widget.ConstraintLayout>
- </androidx.cardview.widget.CardView>
-</LinearLayout>
\ No newline at end of file
+ <androidx.appcompat.widget.AppCompatCheckBox
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp" />
+ </androidx.appcompat.widget.LinearLayoutCompat>
+</androidx.cardview.widget.CardView>
\ No newline at end of file
diff --git a/benchmark/integration-tests/macrobenchmark/build.gradle b/benchmark/integration-tests/macrobenchmark/build.gradle
index 3384fe8..58fb8e8 100644
--- a/benchmark/integration-tests/macrobenchmark/build.gradle
+++ b/benchmark/integration-tests/macrobenchmark/build.gradle
@@ -30,6 +30,7 @@
android {
defaultConfig {
minSdkVersion 28
+ testInstrumentationRunnerArgument 'androidx.benchmark.output.enable', 'true'
}
}
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml b/benchmark/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
index 2f8615f..6dd48d3 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
@@ -28,4 +28,5 @@
<queries>
<package android:name="androidx.benchmark.integration.macrobenchmark.target" />
</queries>
+ <application android:requestLegacyExternalStorage="true"/>
</manifest>
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 4a88391..0586e56 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -18,8 +18,10 @@
import android.content.Intent
import android.util.Log
+import androidx.benchmark.BenchmarkResult
import androidx.benchmark.InstrumentationResults
-import androidx.benchmark.Stats
+import androidx.benchmark.MetricResult
+import androidx.benchmark.ResultWriter
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
@@ -93,12 +95,15 @@
* This function is a building block for public testing APIs
*/
fun macrobenchmark(
- benchmarkName: String,
+ uniqueName: String,
+ className: String,
+ testName: String,
config: MacrobenchmarkConfig,
launchWithClearTask: Boolean,
setupBlock: MacrobenchmarkScope.(Boolean) -> Unit,
measureBlock: MacrobenchmarkScope.() -> Unit
) = withPermissiveSeLinuxPolicy {
+ val startTime = System.nanoTime()
val scope = MacrobenchmarkScope(config.packageName, launchWithClearTask)
// always kill the process at beginning of test
@@ -117,7 +122,7 @@
it.configure(config)
}
var isFirstRun = true
- val results = List(config.iterations) { iteration ->
+ val metricResults = List(config.iterations) { iteration ->
setupBlock(scope, isFirstRun)
isFirstRun = false
try {
@@ -130,7 +135,7 @@
config.metrics.forEach {
it.stop()
}
- perfettoCollector.stop(benchmarkName, iteration)
+ perfettoCollector.stop(uniqueName, iteration)
}
config.metrics
@@ -138,32 +143,55 @@
.map { it.getMetrics(config.packageName) }
// merge into one map
.reduce { sum, element -> sum + element }
- }
-
- // merge each independent Map<String,Long> to one Map<String,List<Long>>
- val setOfAllKeys = results.flatMap { it.keys }.toSet()
- val listResults = setOfAllKeys.map { key ->
- // b/174175947
- key to results.mapNotNull {
- if (key !in it) {
- Log.w(TAG, "Value $key missing from one iteration {$it}")
- }
- it[key]
- }
- }.toMap()
- val statsList = listResults.map { (metricName, values) ->
- Stats(values.toLongArray(), metricName)
- }.sortedBy { it.name }
+ }.mergeToMetricResults()
InstrumentationResults.instrumentationReport {
- ideSummaryRecord(ideSummaryString(benchmarkName, statsList))
+ val statsList = metricResults.map { it.stats }
+ ideSummaryRecord(ideSummaryString(uniqueName, statsList))
statsList.forEach { it.putInBundle(bundle, "") }
}
+
+ val warmupIterations = if (config.compilationMode is CompilationMode.SpeedProfile) {
+ config.compilationMode.warmupIterations
+ } else {
+ 0
+ }
+
+ ResultWriter.appendReport(
+ BenchmarkResult(
+ className = className,
+ testName = testName,
+ totalRunTimeNs = System.nanoTime() - startTime,
+ metrics = metricResults,
+ repeatIterations = config.iterations,
+ thermalThrottleSleepSeconds = 0,
+ warmupIterations = warmupIterations
+ )
+ )
} finally {
scope.killProcess()
}
}
+/**
+ * Merge the Map<String, Long> results from each iteration into one Map<MetricResult>
+ */
+private fun List<Map<String, Long>>.mergeToMetricResults(): List<MetricResult> {
+ val setOfAllKeys = flatMap { it.keys }.toSet()
+ val listResults = setOfAllKeys.map { key ->
+ // b/174175947
+ key to mapNotNull {
+ if (key !in it) {
+ Log.w(TAG, "Value $key missing from one iteration {$it}")
+ }
+ it[key]
+ }
+ }.toMap()
+ return listResults.map { (metricName, values) ->
+ MetricResult(metricName, values.toLongArray())
+ }.sortedBy { it.stats.name }
+}
+
enum class StartupMode {
/**
* Startup from scratch - app's process is not alive, and must be started in addition to
@@ -193,13 +221,17 @@
}
fun startupMacrobenchmark(
- benchmarkName: String,
+ uniqueName: String,
+ className: String,
+ testName: String,
config: MacrobenchmarkConfig,
startupMode: StartupMode,
performStartup: MacrobenchmarkScope.() -> Unit
) {
macrobenchmark(
- benchmarkName = benchmarkName,
+ uniqueName = uniqueName,
+ className = className,
+ testName = testName,
config = config,
setupBlock = { firstIterAfterCompile ->
if (startupMode == StartupMode.COLD) {
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkRule.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkRule.kt
index a5edac8..0bc18a3 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkRule.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkRule.kt
@@ -24,7 +24,7 @@
* JUnit rule for benchmarking large app operations like startup.
*/
class MacrobenchmarkRule : TestRule {
- lateinit var benchmarkName: String
+ lateinit var currentDescription: Description
fun measureRepeated(
config: MacrobenchmarkConfig,
@@ -32,7 +32,9 @@
measureBlock: MacrobenchmarkScope.() -> Unit
) {
macrobenchmark(
- benchmarkName = benchmarkName,
+ uniqueName = currentDescription.toUniqueName(),
+ className = currentDescription.className,
+ testName = currentDescription.methodName,
config = config,
launchWithClearTask = true,
setupBlock = setupBlock,
@@ -46,7 +48,9 @@
performStartup: MacrobenchmarkScope.() -> Unit
) {
startupMacrobenchmark(
- benchmarkName = benchmarkName,
+ uniqueName = currentDescription.toUniqueName(),
+ className = currentDescription.className,
+ testName = currentDescription.methodName,
config = config,
startupMode = startupMode,
performStartup = performStartup
@@ -55,7 +59,7 @@
override fun apply(base: Statement, description: Description) = object : Statement() {
override fun evaluate() {
- benchmarkName = description.toUniqueName()
+ currentDescription = description
base.evaluate()
}
}
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/XmlTestConfigVerificationTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/XmlTestConfigVerificationTest.kt
deleted file mode 100644
index 43a6fe0..0000000
--- a/buildSrc-tests/src/test/kotlin/androidx/build/XmlTestConfigVerificationTest.kt
+++ /dev/null
@@ -1,73 +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.build
-
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.xml.sax.InputSource
-import org.xml.sax.helpers.DefaultHandler
-import java.io.StringReader
-import javax.xml.parsers.SAXParserFactory
-
-/**
- * Simple check that the test config templates are able to be parsed as valid xml.
- */
-@RunWith(JUnit4::class)
-class XmlTestConfigVerificationTest {
-
- @Test
- fun testValidTestConfigXml_TEMPLATE() {
- val parser = SAXParserFactory.newInstance().newSAXParser()
- parser.parse(
- InputSource(StringReader(TEMPLATE.replace("TEST_BLOCK", FULL_TEST))),
- DefaultHandler()
- )
- }
-
- @Test
- fun testValidTestConfigXml_SELF_INSTRUMENTING_TEMPLATE() {
- val parser = SAXParserFactory.newInstance().newSAXParser()
- parser.parse(
- InputSource(
- StringReader(
- SELF_INSTRUMENTING_TEMPLATE.replace(
- "TEST_BLOCK",
- DEPENDENT_TESTS
- )
- )
- ),
- DefaultHandler()
- )
- }
-
- @Test
- fun testValidTestConfigXml_MEDIA_TEMPLATE() {
- val parser = SAXParserFactory.newInstance().newSAXParser()
- parser.parse(
- InputSource(
- StringReader(
- MEDIA_TEMPLATE.replace(
- "INSTRUMENTATION_ARGS",
- CLIENT_PREVIOUS + SERVICE_PREVIOUS
- )
- )
- ),
- DefaultHandler()
- )
- }
-}
\ No newline at end of file
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/dependencyTracker/AffectedModuleDetectorImplTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/dependencyTracker/AffectedModuleDetectorImplTest.kt
index c187d48..cef9eac 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/dependencyTracker/AffectedModuleDetectorImplTest.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/dependencyTracker/AffectedModuleDetectorImplTest.kt
@@ -48,7 +48,6 @@
val tmpFolder2 = TemporaryFolder()
private lateinit var root: Project
- private lateinit var root2: Project
private lateinit var p1: Project
private lateinit var p2: Project
private lateinit var p3: Project
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/XmlTestConfigVerificationTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/XmlTestConfigVerificationTest.kt
new file mode 100644
index 0000000..d35f5e8
--- /dev/null
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/XmlTestConfigVerificationTest.kt
@@ -0,0 +1,208 @@
+/*
+ * 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.build.testConfiguration
+
+import org.hamcrest.CoreMatchers
+import org.hamcrest.MatcherAssert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.xml.sax.InputSource
+import org.xml.sax.helpers.DefaultHandler
+import java.io.StringReader
+import javax.xml.parsers.SAXParserFactory
+
+/**
+ * Simple check that the test config templates are able to be parsed as valid xml.
+ */
+@RunWith(JUnit4::class)
+class XmlTestConfigVerificationTest {
+
+ private lateinit var builder: ConfigBuilder
+ private lateinit var mediaBuilder: MediaConfigBuilder
+
+ @Before
+ fun init() {
+ builder = ConfigBuilder()
+ builder.isBenchmark(false)
+ .applicationId("com.androidx.placeholder.Placeholder")
+ .isPostsubmit(true)
+ .minSdk("15")
+ .tag("placeholder_tag")
+ .testApkName("placeholder.apk")
+ .testRunner("com.example.Runner")
+ mediaBuilder = MediaConfigBuilder()
+ mediaBuilder.clientApplicationId("com.androidx.client.Placeholder")
+ .clientApkName("clientPlaceholder.apk")
+ .serviceApplicationId("com.androidx.service.Placeholder")
+ .serviceApkName("servicePlaceholder.apk")
+ .minSdk("15")
+ .tag("placeholder_tag")
+ .testRunner("com.example.Runner")
+ .isClientPrevious(true)
+ .isServicePrevious(false)
+ }
+
+ @Test
+ fun testAgainstGoldenDefault() {
+ MatcherAssert.assertThat(
+ builder.build(),
+ CoreMatchers.`is`(goldenDefaultConfig)
+ )
+ }
+
+ @Test
+ fun testAgainstMediaGoldenDefault() {
+ MatcherAssert.assertThat(
+ mediaBuilder.build(),
+ CoreMatchers.`is`(goldenMediaDefaultConfig)
+ )
+ }
+
+ @Test
+ fun testValidTestConfigXml_default() {
+ validate(builder.build())
+ }
+
+ @Test
+ fun testValidTestConfigXml_benchmarkTrue() {
+ builder.isBenchmark(true)
+ validate(builder.build())
+ }
+
+ @Test
+ fun testValidTestConfigXml_withAppApk() {
+ builder.appApkName("Placeholder.apk")
+ validate(builder.build())
+ }
+
+ @Test
+ fun testValidTestConfigXml_presubmitWithAppApk() {
+ builder.isPostsubmit(false)
+ .appApkName("Placeholder.apk")
+ validate(builder.build())
+ }
+
+ @Test
+ fun testValidTestConfigXml_presubmit() {
+ builder.isPostsubmit(false)
+ validate(builder.build())
+ }
+
+ @Test
+ fun testValidTestConfigXml_presubmitBenchmark() {
+ builder.isPostsubmit(false)
+ .isBenchmark(true)
+ validate(builder.build())
+ }
+
+ @Test
+ fun testValidMediaConfigXml_default() {
+ validate(mediaBuilder.build())
+ }
+
+ @Test
+ fun testValidMediaConfigXml_presubmit() {
+ mediaBuilder.isPostsubmit(false)
+ validate(mediaBuilder.build())
+ }
+
+ private fun validate(xml: String) {
+ val parser = SAXParserFactory.newInstance().newSAXParser()
+ return parser.parse(
+ InputSource(
+ StringReader(
+ xml
+ )
+ ),
+ DefaultHandler()
+ )
+ }
+}
+
+private val goldenDefaultConfig = """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- Copyright (C) 2020 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License")
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions
+ and limitations under the License.-->
+ <configuration description="Runs tests for the module">
+ <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MinApiLevelModuleController">
+ <option name="min-api-level" value="15" />
+ </object>
+ <option name="test-suite-tag" value="placeholder_tag" />
+ <option name="config-descriptor:metadata" key="applicationId" value="com.androidx.placeholder.Placeholder" />
+ <option name="wifi:disable" value="true" />
+ <include name="google/unbundled/common/setup" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="placeholder.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+ <option name="runner" value="com.example.Runner"/>
+ <option name="package" value="com.androidx.placeholder.Placeholder" />
+ </test>
+ </configuration>
+""".trimIndent()
+
+private val goldenMediaDefaultConfig = """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- Copyright (C) 2020 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License")
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions
+ and limitations under the License.-->
+ <configuration description="Runs tests for the module">
+ <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MinApiLevelModuleController">
+ <option name="min-api-level" value="15" />
+ </object>
+ <option name="test-suite-tag" value="placeholder_tag" />
+ <option name="test-suite-tag" value="media_compat" />
+ <option name="config-descriptor:metadata" key="applicationId" value="com.androidx.client.Placeholder;com.androidx.service.Placeholder" />
+ <option name="wifi:disable" value="true" />
+ <include name="google/unbundled/common/setup" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="clientPlaceholder.apk" />
+ <option name="test-file-name" value="servicePlaceholder.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+ <option name="runner" value="com.example.Runner"/>
+ <option name="package" value="com.androidx.client.Placeholder" />
+ <option name="instrumentation-arg" key="client_version" value="previous" />
+ <option name="instrumentation-arg" key="service_version" value="tot" />
+ </test>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+ <option name="runner" value="com.example.Runner"/>
+ <option name="package" value="com.androidx.service.Placeholder" />
+ <option name="instrumentation-arg" key="client_version" value="previous" />
+ <option name="instrumentation-arg" key="service_version" value="tot" />
+ </test>
+ </configuration>
+""".trimIndent()
\ No newline at end of file
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index bad8aa5..b2122db5 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -60,9 +60,7 @@
cacheableImplementation {
extendsFrom(project.configurations.cacheableApi)
}
- cacheableRuntime {
- extendsFrom(project.configurations.cacheableImplementation)
- }
+ cacheableRuntimeOnly
}
dependencies {
@@ -74,12 +72,12 @@
cacheableApi build_libs.dokka_gradle
// needed by inspection plugin
cacheableImplementation "com.google.protobuf:protobuf-gradle-plugin:0.8.13"
- // TODO(aurimas): remove when b/173417030 is fixed
+ // TODO(aurimas): remove when b/174658825 is fixed
cacheableImplementation "org.anarres.jarjar:jarjar-gradle:1.0.1"
cacheableImplementation "com.github.jengelman.gradle.plugins:shadow:5.2.0"
// dependencies that aren't used by buildSrc directly but that we resolve here so that the
// root project doesn't need to re-resolve them and their dependencies on every build
- cacheableRuntime build_libs.hilt_plugin
+ cacheableRuntimeOnly build_libs.hilt_plugin
// dependencies whose resolutions we don't need to cache
compileOnly(findGradleKotlinDsl()) // Only one file in this configuration, no need to cache it
implementation project("jetpad-integration") // Doesn't have a .pom, so not slow to load
@@ -168,7 +166,7 @@
loadConfigurationQuicklyInto(configurations.cacheableApi, configurations.api)
loadConfigurationQuicklyInto(configurations.cacheableImplementation, configurations.implementation)
-loadConfigurationQuicklyInto(configurations.cacheableRuntime, configurations.runtime)
+loadConfigurationQuicklyInto(configurations.cacheableRuntimeOnly, configurations.runtimeOnly)
project.tasks.withType(Jar) { task ->
task.reproducibleFileOrder = true
diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt
index 5fa294c..09e1a67 100644
--- a/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt
@@ -33,6 +33,8 @@
import androidx.build.jacoco.Jacoco
import androidx.build.license.configureExternalDependencyLicenseCheck
import androidx.build.studio.StudioTask
+import androidx.build.testConfiguration.addAppApkToTestConfigGeneration
+import androidx.build.testConfiguration.configureTestConfigGeneration
import com.android.build.api.extension.LibraryAndroidComponentsExtension
import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXUiPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXUiPlugin.kt
index f27e132..600632c 100644
--- a/buildSrc/src/main/kotlin/androidx/build/AndroidXUiPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXUiPlugin.kt
@@ -21,7 +21,6 @@
import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.LibraryPlugin
import com.android.build.gradle.TestedExtension
-import org.gradle.api.DomainObjectCollection
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.type.ArtifactTypeDefinition
@@ -32,7 +31,6 @@
import org.gradle.kotlin.dsl.invoke
import org.jetbrains.kotlin.gradle.plugin.KotlinBasePluginWrapper
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper
-import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
const val composeSourceOption =
@@ -194,7 +192,10 @@
"src/commonMain/kotlin", "src/jvmMain/kotlin",
"src/androidMain/kotlin"
)
- res.srcDirs("src/androidMain/res")
+ res.srcDirs(
+ "src/commonMain/resources",
+ "src/androidMain/res"
+ )
// Keep Kotlin files in java source sets so the source set is not empty when
// running unit tests which would prevent the tests from running in CI.
@@ -225,11 +226,9 @@
* resolved.
*/
private fun Project.configureForMultiplatform() {
- if (multiplatformExtension == null) {
- throw IllegalStateException(
- "Unable to configureForMultiplatform() when " +
- "multiplatformExtension is null (multiplatform plugin not enabled?)"
- )
+ val multiplatformExtension = checkNotNull(multiplatformExtension) {
+ "Unable to configureForMultiplatform() when " +
+ "multiplatformExtension is null (multiplatform plugin not enabled?)"
}
/*
@@ -247,13 +246,20 @@
TODO: Consider changing unitTest to androidLocalTest and androidAndroidTest to
androidDeviceTest when https://github.com/JetBrains/kotlin/pull/2829 rolls in.
*/
- multiplatformExtension!!.sourceSets {
+ multiplatformExtension.sourceSets.all {
// Allow all experimental APIs, since MPP projects are themselves experimental
- (this as DomainObjectCollection<KotlinSourceSet>).all {
- it.languageSettings.apply {
- useExperimentalAnnotation("kotlin.Experimental")
- useExperimentalAnnotation("kotlin.ExperimentalMultiplatform")
- }
+ it.languageSettings.apply {
+ useExperimentalAnnotation("kotlin.Experimental")
+ useExperimentalAnnotation("kotlin.ExperimentalMultiplatform")
+ }
+ }
+
+ afterEvaluate {
+ if (multiplatformExtension.targets.findByName("jvm") != null) {
+ tasks.named("jvmTestClasses").also(::addToBuildOnServer)
+ }
+ if (multiplatformExtension.targets.findByName("desktop") != null) {
+ tasks.named("desktopTestClasses").also(::addToBuildOnServer)
}
}
}
diff --git a/buildSrc/src/main/kotlin/androidx/build/GenerateMediaTestConfigurationTask.kt b/buildSrc/src/main/kotlin/androidx/build/GenerateMediaTestConfigurationTask.kt
deleted file mode 100644
index 3bfbd2f..0000000
--- a/buildSrc/src/main/kotlin/androidx/build/GenerateMediaTestConfigurationTask.kt
+++ /dev/null
@@ -1,219 +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.build
-
-import com.android.build.api.variant.BuiltArtifacts
-import com.android.build.api.variant.BuiltArtifactsLoader
-import org.gradle.api.DefaultTask
-import org.gradle.api.file.DirectoryProperty
-import org.gradle.api.file.RegularFileProperty
-import org.gradle.api.provider.Property
-import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.InputFiles
-import org.gradle.api.tasks.Internal
-import org.gradle.api.tasks.OutputFile
-import org.gradle.api.tasks.TaskAction
-import java.io.File
-
-const val MEDIA_TEMPLATE = """<?xml version="1.0" encoding="utf-8"?>
- <!-- Copyright (C) 2019 The Android Open Source Project
- Licensed under the Apache License, Version 2.0 (the "License")
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
- <configuration description="Runs tests for the module">
- <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MinApiLevelModuleController">
- <option name="min-api-level" value="MIN_SDK" />
- </object>
- <option name="test-suite-tag" value="androidx_unit_tests_suite" />
- <option name="config-descriptor:metadata" key="applicationId"
- value="CLIENT_APPLICATION_ID;SERVICE_APPLICATION_ID" />
- <option name="wifi:disable" value="true" />
- <include name="google/unbundled/common/setup" />
- <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
- <option name="cleanup-apks" value="true" />
- <option name="test-file-name" value="CLIENT_FILE_NAME" />
- <option name="test-file-name" value="SERVICE_FILE_NAME" />
- </target_preparer>
- <test class="com.android.tradefed.testtype.AndroidJUnitTest">
- <option name="runner" value="TEST_RUNNER"/>
- <option name="package" value="CLIENT_APPLICATION_ID" />
- INSTRUMENTATION_ARGS
- </test>
- <test class="com.android.tradefed.testtype.AndroidJUnitTest">
- <option name="runner" value="TEST_RUNNER"/>
- <option name="package" value="SERVICE_APPLICATION_ID" />
- INSTRUMENTATION_ARGS
- </test>
- </configuration>"""
-
-const val CLIENT_PREVIOUS = """
- <option name="instrumentation-arg" key="client_version" value="previous" />
-"""
-const val CLIENT_TOT = """
- <option name="instrumentation-arg" key="client_version" value="tot" />
-"""
-const val SERVICE_PREVIOUS = """
- <option name="instrumentation-arg" key="service_version" value="previous" />
-"""
-const val SERVICE_TOT = """
- <option name="instrumentation-arg" key="service_version" value="tot" />
-"""
-
-/**
- * Writes three configuration files to test combinations of media client & service in
- * <a href=https://source.android.com/devices/tech/test_infra/tradefed/testing/through-suite/android-test-structure>AndroidTest.xml</a>
- * format that gets zipped alongside the APKs to be tested. The combinations are of previous and
- * tip-of-tree versions client and service. We want to test every possible pairing that includes
- * tip-of-tree.
- *
- * This config gets ingested by Tradefed.
- */
-abstract class GenerateMediaTestConfigurationTask : DefaultTask() {
-
- @get:InputFiles
- abstract val clientToTFolder: DirectoryProperty
-
- @get:Internal
- abstract val clientToTLoader: Property<BuiltArtifactsLoader>
-
- @get:InputFiles
- abstract val clientPreviousFolder: DirectoryProperty
-
- @get:Internal
- abstract val clientPreviousLoader: Property<BuiltArtifactsLoader>
-
- @get:InputFiles
- abstract val serviceToTFolder: DirectoryProperty
-
- @get:Internal
- abstract val serviceToTLoader: Property<BuiltArtifactsLoader>
-
- @get:InputFiles
- abstract val servicePreviousFolder: DirectoryProperty
-
- @get:Internal
- abstract val servicePreviousLoader: Property<BuiltArtifactsLoader>
-
- @get:Input
- abstract val clientToTPath: Property<String>
-
- @get:Input
- abstract val clientPreviousPath: Property<String>
-
- @get:Input
- abstract val serviceToTPath: Property<String>
-
- @get:Input
- abstract val servicePreviousPath: Property<String>
-
- @get:Input
- abstract val minSdk: Property<Int>
-
- @get:Input
- abstract val testRunner: Property<String>
-
- @get:OutputFile
- abstract val clientPreviousServiceToT: RegularFileProperty
-
- @get:OutputFile
- abstract val clientToTServicePrevious: RegularFileProperty
-
- @get:OutputFile
- abstract val clientToTServiceToT: RegularFileProperty
-
- @TaskAction
- fun generateAndroidTestZip() {
- val clientToTApk = resolveApk(clientToTFolder, clientToTLoader)
- val clientPreviousApk = resolveApk(clientPreviousFolder, clientPreviousLoader)
- val serviceToTApk = resolveApk(serviceToTFolder, serviceToTLoader)
- val servicePreviousApk = resolveApk(
- servicePreviousFolder, servicePreviousLoader
- )
- writeConfigFileContent(
- clientToTApk, serviceToTApk, clientToTPath.get(),
- serviceToTPath.get(), clientToTServiceToT
- )
- writeConfigFileContent(
- clientToTApk, servicePreviousApk, clientToTPath.get(),
- servicePreviousPath.get(), clientToTServicePrevious
- )
- writeConfigFileContent(
- clientPreviousApk, serviceToTApk, clientPreviousPath.get(),
- serviceToTPath.get(), clientPreviousServiceToT
- )
- }
-
- private fun resolveApk(
- apkFolder: DirectoryProperty,
- apkLoader: Property<BuiltArtifactsLoader>
- ): BuiltArtifacts {
- return apkLoader.get().load(apkFolder.get())
- ?: throw RuntimeException("Cannot load APK for $name")
- }
-
- private fun resolveName(apk: BuiltArtifacts, path: String): String {
- return apk.elements.single().outputFile.substringAfterLast("/")
- .renameApkForTesting(path, false)
- }
-
- private fun writeConfigFileContent(
- clientApk: BuiltArtifacts,
- serviceApk: BuiltArtifacts,
- clientPath: String,
- servicePath: String,
- outputFile: RegularFileProperty
- ) {
- val instrumentationArgs =
- if (clientPath.contains("previous")) {
- if (servicePath.contains("previous")) {
- CLIENT_PREVIOUS + SERVICE_PREVIOUS
- } else {
- CLIENT_PREVIOUS + SERVICE_TOT
- }
- } else if (servicePath.contains("previous")) {
- CLIENT_TOT + SERVICE_PREVIOUS
- } else {
- CLIENT_TOT + SERVICE_TOT
- }
- var configContent: String = MEDIA_TEMPLATE
- configContent = configContent
- .replace("CLIENT_FILE_NAME", resolveName(clientApk, clientPath))
- .replace("SERVICE_FILE_NAME", resolveName(serviceApk, servicePath))
- .replace("CLIENT_APPLICATION_ID", clientApk.applicationId)
- .replace("SERVICE_APPLICATION_ID", serviceApk.applicationId)
- .replace("MIN_SDK", minSdk.get().toString())
- .replace("TEST_RUNNER", testRunner.get())
- .replace("INSTRUMENTATION_ARGS", instrumentationArgs)
- val resolvedOutputFile: File = outputFile.asFile.get()
- if (!resolvedOutputFile.exists()) {
- if (!resolvedOutputFile.createNewFile()) {
- throw RuntimeException(
- "Failed to create test configuration file: $outputFile"
- )
- }
- }
- resolvedOutputFile.writeText(configContent)
- }
-}
diff --git a/buildSrc/src/main/kotlin/androidx/build/GenerateTestConfigurationTask.kt b/buildSrc/src/main/kotlin/androidx/build/GenerateTestConfigurationTask.kt
deleted file mode 100644
index e958cd3..0000000
--- a/buildSrc/src/main/kotlin/androidx/build/GenerateTestConfigurationTask.kt
+++ /dev/null
@@ -1,210 +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.build
-
-import androidx.build.dependencyTracker.ProjectSubset
-import com.android.build.api.variant.BuiltArtifactsLoader
-import org.gradle.api.DefaultTask
-import org.gradle.api.file.DirectoryProperty
-import org.gradle.api.file.RegularFileProperty
-import org.gradle.api.provider.Property
-import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.InputFiles
-import org.gradle.api.tasks.Internal
-import org.gradle.api.tasks.Optional
-import org.gradle.api.tasks.OutputFile
-import org.gradle.api.tasks.TaskAction
-import java.io.File
-
-const val TEMPLATE = """<?xml version="1.0" encoding="utf-8"?>
- <!-- Copyright (C) 2019 The Android Open Source Project
- Licensed under the Apache License, Version 2.0 (the "License")
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
- <configuration description="Runs tests for the module">
- <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MinApiLevelModuleController">
- <option name="min-api-level" value="MIN_SDK" />
- </object>
- <option name="test-suite-tag" value="TEST_SUITE_TAG" />
- <option name="config-descriptor:metadata" key="applicationId" value="APPLICATION_ID" />
- <option name="wifi:disable" value="true" />
- <include name="google/unbundled/common/setup" />
- <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
- <option name="cleanup-apks" value="true" />
- <option name="test-file-name" value="TEST_FILE_NAME" />
- <option name="test-file-name" value="APP_FILE_NAME" />
- </target_preparer>
- <test class="com.android.tradefed.testtype.AndroidJUnitTest">
- <option name="runner" value="TEST_RUNNER"/>
- <option name="package" value="APPLICATION_ID" />
- </test>
- </configuration>"""
-
-const val SELF_INSTRUMENTING_TEMPLATE = """<?xml version="1.0" encoding="utf-8"?>
- <!-- Copyright (C) 2019 The Android Open Source Project
- Licensed under the Apache License, Version 2.0 (the "License")
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
- <configuration description="Runs tests for the module">
- <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MinApiLevelModuleController">
- <option name="min-api-level" value="MIN_SDK" />
- </object>
- <option name="test-suite-tag" value="TEST_SUITE_TAG" />
- <option name="config-descriptor:metadata" key="applicationId" value="APPLICATION_ID" />
- <option name="wifi:disable" value="true" />
- <include name="google/unbundled/common/setup" />
- <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
- <option name="cleanup-apks" value="true" />
- <option name="test-file-name" value="TEST_FILE_NAME" />
- </target_preparer>
- TEST_BLOCK
- </configuration>"""
-
-const val FULL_TEST = """
- <test class="com.android.tradefed.testtype.AndroidJUnitTest">
- <option name="runner" value="TEST_RUNNER"/>
- <option name="package" value="APPLICATION_ID" />
- </test>
-"""
-
-const val DEPENDENT_TESTS = """
- <test class="com.android.tradefed.testtype.AndroidJUnitTest">
- <option name="runner" value="TEST_RUNNER"/>
- <option name="package" value="APPLICATION_ID" />
- <option name="size" value="small" />
- <option name="test-timeout" value="300" />
- </test>
- <test class="com.android.tradefed.testtype.AndroidJUnitTest">
- <option name="runner" value="TEST_RUNNER"/>
- <option name="package" value="APPLICATION_ID" />
- <option name="size" value="medium" />
- <option name="test-timeout" value="1500" />
- </test>
-"""
-
-/**
- * Writes a configuration file in
- * <a href=https://source.android.com/devices/tech/test_infra/tradefed/testing/through-suite/android-test-structure>AndroidTest.xml</a>
- * format that gets zipped alongside the APKs to be tested.
- * This config gets ingested by Tradefed.
- */
-abstract class GenerateTestConfigurationTask : DefaultTask() {
-
- @get:InputFiles
- @get:Optional
- abstract val appFolder: DirectoryProperty
-
- @get:Internal
- abstract val appLoader: Property<BuiltArtifactsLoader>
-
- @get:InputFiles
- abstract val testFolder: DirectoryProperty
-
- @get:Internal
- abstract val testLoader: Property<BuiltArtifactsLoader>
-
- @get:Input
- abstract val minSdk: Property<Int>
-
- @get:Input
- abstract val hasBenchmarkPlugin: Property<Boolean>
-
- @get:Input
- abstract val testRunner: Property<String>
-
- @get:Input
- abstract val projectPath: Property<String>
-
- @get:Input
- abstract val affectedModuleDetectorSubset: Property<ProjectSubset>
-
- @get:OutputFile
- abstract val outputXml: RegularFileProperty
-
- @TaskAction
- fun generateAndroidTestZip() {
- writeConfigFileContent()
- }
-
- private fun writeConfigFileContent() {
- /*
- Testing an Android Application project involves 2 APKS: an application to be instrumented,
- and a test APK. Testing an Android Library project involves only 1 APK, since the library
- is bundled inside the test APK, meaning it is self instrumenting. We add extra data to
- configurations testing Android Application projects, so that both APKs get installed.
- */
- var configContent: String = if (appLoader.isPresent) {
- val appApk = appLoader.get().load(appFolder.get())
- ?: throw RuntimeException("Cannot load application APK for $name")
- val appName = appApk.elements.single().outputFile.substringAfterLast("/")
- .renameApkForTesting(projectPath.get(), hasBenchmarkPlugin.get())
- TEMPLATE.replace("APP_FILE_NAME", appName)
- } else {
- SELF_INSTRUMENTING_TEMPLATE
- }
- configContent = when (affectedModuleDetectorSubset.get()) {
- ProjectSubset.CHANGED_PROJECTS, ProjectSubset.ALL_AFFECTED_PROJECTS -> {
- configContent.replace("TEST_BLOCK", FULL_TEST)
- }
- ProjectSubset.DEPENDENT_PROJECTS -> {
- configContent.replace("TEST_BLOCK", DEPENDENT_TESTS)
- }
- else -> {
- throw IllegalStateException(
- "$name should not be running if the AffectedModuleDetector is returning " +
- "${affectedModuleDetectorSubset.get()} for this project."
- )
- }
- }
- val tag = if (hasBenchmarkPlugin.get()) "MetricTests" else "androidx_unit_tests"
- val testApk = testLoader.get().load(testFolder.get())
- ?: throw RuntimeException("Cannot load test APK for $name")
- val testName = testApk.elements.single().outputFile
- .substringAfterLast("/")
- .renameApkForTesting(projectPath.get(), hasBenchmarkPlugin.get())
- configContent = configContent.replace("TEST_FILE_NAME", testName)
- .replace("APPLICATION_ID", testApk.applicationId)
- .replace("MIN_SDK", minSdk.get().toString())
- .replace("TEST_SUITE_TAG", tag)
- .replace("TEST_RUNNER", testRunner.get())
- val resolvedOutputFile: File = outputXml.asFile.get()
- if (!resolvedOutputFile.exists()) {
- if (!resolvedOutputFile.createNewFile()) {
- throw RuntimeException(
- "Failed to create test configuration file: $outputXml"
- )
- }
- }
- resolvedOutputFile.writeText(configContent)
- }
-}
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index c129ddc..6c009b3 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -71,8 +71,9 @@
val IPC = Version("1.0.0-alpha01")
val JETIFIER = Version("1.0.0-beta10")
val LEANBACK = Version("1.1.0-beta01")
- val LEANBACK_PAGING = Version("1.1.0-alpha06")
+ val LEANBACK_PAGING = Version("1.1.0-alpha07")
val LEANBACK_PREFERENCE = Version("1.1.0-beta01")
+ val LEANBACK_TAB = Version("1.1.0-beta01")
val LEGACY = Version("1.1.0-alpha01")
val LOCALBROADCASTMANAGER = Version("1.1.0-alpha02")
val LIFECYCLE = Version("2.3.0-rc01")
@@ -83,7 +84,7 @@
val MEDIAROUTER = Version("1.3.0-alpha01")
val NAVIGATION = Version("2.4.0-alpha01")
val NAVIGATION_COMPOSE = Version("1.0.0-alpha04")
- val PAGING = Version("3.0.0-alpha10")
+ val PAGING = Version("3.0.0-alpha11")
val PAGING_COMPOSE = Version("1.0.0-alpha04")
val PALETTE = Version("1.1.0-alpha01")
val PRINT = Version("1.1.0-beta01")
@@ -96,10 +97,11 @@
val ROOM = Version("2.3.0-alpha04")
val SAVEDSTATE = Version("1.1.0-rc01")
val SECURITY = Version("1.1.0-alpha03")
+ val SECURITY_APP_AUTHENTICATOR = Version("1.0.0-alpha01")
val SECURITY_BIOMETRIC = Version("1.0.0-alpha01")
val SECURITY_IDENTITY_CREDENTIAL = Version("1.0.0-alpha01")
val SERIALIZATION = Version("1.0.0-alpha01")
- val SHARETARGET = Version("1.1.0-rc01")
+ val SHARETARGET = Version("1.2.0-alpha01")
val SLICE = Version("1.1.0-alpha02")
val SLICE_BENCHMARK = Version("1.1.0-alpha02")
val SLICE_BUILDERS_KTX = Version("1.0.0-alpha08")
@@ -121,15 +123,15 @@
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-alpha03")
- val WEAR_COMPLICATIONS = Version("1.0.0-alpha03")
- val WEAR_INPUT = Version("1.0.0-rc01")
+ val WEAR = Version("1.2.0-alpha04")
+ val WEAR_COMPLICATIONS = Version("1.0.0-alpha04")
+ val WEAR_INPUT = Version("1.1.0-alpha01")
val WEAR_TILES = Version("1.0.0-alpha01")
val WEAR_TILES_DATA = WEAR_TILES
- val WEAR_WATCHFACE = Version("1.0.0-alpha03")
- val WEAR_WATCHFACE_CLIENT = Version("1.0.0-alpha03")
- val WEAR_WATCHFACE_DATA = Version("1.0.0-alpha03")
- val WEAR_WATCHFACE_STYLE = Version("1.0.0-alpha03")
+ val WEAR_WATCHFACE = Version("1.0.0-alpha04")
+ val WEAR_WATCHFACE_CLIENT = Version("1.0.0-alpha04")
+ val WEAR_WATCHFACE_DATA = Version("1.0.0-alpha04")
+ val WEAR_WATCHFACE_STYLE = Version("1.0.0-alpha04")
val WEBKIT = Version("1.4.0-beta01")
val WINDOW = Version("1.0.0-alpha02")
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 e764c62..9c6b2c0 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -110,10 +110,15 @@
// concerned with drawables potentially being a little bit blurry
disable("IconMissingDensityFolder")
+ // Disable a check that's only triggered by translation updates which are
+ // outside of library owners' control, b/174655193
+ disable("UnusedQuantity")
+
// Disable until it works for our projects, b/171986505
disable("JavaPluginLanguageLevel")
- if (extension.type.compilationTarget != CompilationTarget.HOST) {
+ // Provide stricter enforcement for project types intended to run on a device.
+ if (extension.type.compilationTarget == CompilationTarget.DEVICE) {
fatal("Assert")
fatal("NewApi")
fatal("ObsoleteSdkInt")
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
index d8ba393..eba1f5c 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
@@ -77,7 +77,7 @@
"com.squareup:kotlinpoet-classinspector-elements:1.4.0"
const val KOTLIN_COMPILE_TESTING = "com.github.tschuchortdev:kotlin-compile-testing:1.3.1"
const val KOTLIN_COMPILE_TESTING_KSP = "com.github.tschuchortdev:kotlin-compile-testing-ksp:1.3.1"
-const val KSP_VERSION = "1.4.10-dev-experimental-20201120"
+const val KSP_VERSION = "1.4.20-dev-experimental-20201204"
const val KOTLIN_KSP_API = "com.google.devtools.ksp:symbol-processing-api:$KSP_VERSION"
const val KOTLIN_KSP = "com.google.devtools.ksp:symbol-processing:$KSP_VERSION"
const val KOTLIN_GRADLE_PLUGIN = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.20"
@@ -154,6 +154,12 @@
val KOTLIN_TEST_JUNIT get() = "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion"
val KOTLIN_TEST_JS get() = "org.jetbrains.kotlin:kotlin-test-js:$kotlinVersion"
val KOTLIN_REFLECT get() = "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
+val KOTLIN_COMPILER_EMBEDDABLE
+ get() = "org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlinVersion"
+val KOTLIN_COMPILER_DAEMON_EMBEDDABLE
+ get() = "org.jetbrains.kotlin:kotlin-daemon-embeddable:$kotlinVersion"
+val KOTLIN_ANNOTATION_PROCESSING_EMBEDDABLE
+ get() = "org.jetbrains.kotlin:kotlin-annotation-processing-embeddable:$kotlinVersion"
internal lateinit var kotlinCoroutinesVersion: String
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt b/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
index beaed73..199694d 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
@@ -470,12 +470,19 @@
private val COBUILT_TEST_PATHS = setOf(
// Install media tests together per b/128577735
setOf(
+ // Making a change in :media:version-compat-tests makes
+ // mediaGenerateTestConfiguration run (an unfortunate but low priority bug). To
+ // prevent failures from missing apks, we make sure to build the
+ // version-compat-tests projects in that case. Same with media2-session below.
+ ":media:version-compat-tests",
":media:version-compat-tests:client",
":media:version-compat-tests:service",
":media:version-compat-tests:client-previous",
":media:version-compat-tests:service-previous"
),
setOf(
+ ":media2:media2-session",
+ ":media2:media2-session:version-compat-tests",
":media2:media2-session:version-compat-tests:client",
":media2:media2-session:version-compat-tests:service",
":media2:media2-session:version-compat-tests:client-previous",
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/ToStringLogger.kt b/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/ToStringLogger.kt
index 359edbb..eb0d0e0 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/ToStringLogger.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/ToStringLogger.kt
@@ -39,7 +39,7 @@
).also {
it.level = LogLevel.DEBUG
it.setOutputEventListener {
- stringBuilder.appendln(it.toString())
+ stringBuilder.append(it.toString() + "\n")
}
},
Clock {
@@ -65,4 +65,4 @@
return logger
}
}
-}
\ No newline at end of file
+}
diff --git a/buildSrc/src/main/kotlin/androidx/build/jacoco/Jacoco.kt b/buildSrc/src/main/kotlin/androidx/build/jacoco/Jacoco.kt
index 47148e3..2da678f 100644
--- a/buildSrc/src/main/kotlin/androidx/build/jacoco/Jacoco.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/jacoco/Jacoco.kt
@@ -67,7 +67,8 @@
it.from(v.testedVariant.javaCompileProvider.get().destinationDir)
it.exclude("**/R.class", "**/R\$*.class", "**/BuildConfig.class")
it.destinationDirectory.set(project.buildDir)
- it.archiveFileName.set("${project.name}-${v.baseName}-allclasses.jar")
+ val sanitizedPath = project.path.removePrefix(":").replace(':', '_')
+ it.archiveFileName.set("$sanitizedPath-${v.baseName}-allclasses.jar")
}
project.rootProject.tasks.named(
"packageAllClassFilesForCoverageReport",
diff --git a/buildSrc/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt
new file mode 100644
index 0000000..a55e365
--- /dev/null
+++ b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt
@@ -0,0 +1,316 @@
+/*
+ * 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.build.testConfiguration
+
+class ConfigBuilder {
+ var appApkName: String? = null
+ lateinit var applicationId: String
+ var isBenchmark: Boolean = false
+ var isPostsubmit: Boolean = true
+ lateinit var minSdk: String
+ var tag: String = "androidx_unit_tests"
+ lateinit var testApkName: String
+ lateinit var testRunner: String
+
+ fun appApkName(appApkName: String) = apply { this.appApkName = appApkName }
+ fun applicationId(applicationId: String) = apply { this.applicationId = applicationId }
+ fun isBenchmark(isBenchmark: Boolean) = apply { this.isBenchmark = isBenchmark }
+ fun isPostsubmit(isPostsubmit: Boolean) = apply { this.isPostsubmit = isPostsubmit }
+ fun minSdk(minSdk: String) = apply { this.minSdk = minSdk }
+ fun tag(tag: String) = apply { this.tag = tag }
+ fun testApkName(testApkName: String) = apply { this.testApkName = testApkName }
+ fun testRunner(testRunner: String) = apply { this.testRunner = testRunner }
+
+ fun build(): String {
+ val sb = StringBuilder()
+ sb.append(XML_HEADER_AND_LICENSE)
+ .append(CONFIGURATION_OPEN)
+ .append(MIN_API_LEVEL_CONTROLLER_OBJECT.replace("MIN_SDK", minSdk))
+ .append(TEST_SUITE_TAG_OPTION.replace("TEST_SUITE_TAG", tag))
+ .append(MODULE_METADATA_TAG_OPTION.replace("APPLICATION_ID", applicationId))
+ .append(WIFI_DISABLE_OPTION)
+ if (isBenchmark) {
+ if (isPostsubmit) {
+ sb.append(BENCHMARK_POSTSUBMIT_OPTIONS)
+ } else {
+ sb.append(BENCHMARK_PRESUBMIT_OPTION)
+ }
+ }
+ sb.append(SETUP_INCLUDE)
+ .append(TARGET_PREPARER_OPEN)
+ .append(APK_INSTALL_OPTION.replace("APK_NAME", testApkName))
+ if (!appApkName.isNullOrEmpty())
+ sb.append(APK_INSTALL_OPTION.replace("APK_NAME", appApkName!!))
+ sb.append(TARGET_PREPARER_CLOSE)
+ .append(TEST_BLOCK_OPEN)
+ .append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
+ .append(PACKAGE_OPTION.replace("APPLICATION_ID", applicationId))
+ if (isPostsubmit)
+ sb.append(TEST_BLOCK_CLOSE)
+ else {
+ sb.append(SMALL_TEST_OPTIONS)
+ .append(TEST_BLOCK_CLOSE)
+ .append(TEST_BLOCK_OPEN)
+ .append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
+ .append(PACKAGE_OPTION.replace("APPLICATION_ID", applicationId))
+ .append(MEDIUM_TEST_OPTIONS)
+ .append(TEST_BLOCK_CLOSE)
+ }
+ sb.append(CONFIGURATION_CLOSE)
+ return sb.toString()
+ }
+}
+
+class MediaConfigBuilder {
+ lateinit var clientApkName: String
+ lateinit var clientApplicationId: String
+ var isClientPrevious: Boolean = true
+ var isPostsubmit: Boolean = true
+ var isServicePrevious: Boolean = true
+ lateinit var minSdk: String
+ lateinit var serviceApkName: String
+ lateinit var serviceApplicationId: String
+ var tag: String = "androidx_unit_tests"
+ lateinit var testRunner: String
+
+ fun clientApkName(clientApkName: String) = apply { this.clientApkName = clientApkName }
+ fun clientApplicationId(clientApplicationId: String) =
+ apply { this.clientApplicationId = clientApplicationId }
+ fun isPostsubmit(isPostsubmit: Boolean) = apply { this.isPostsubmit = isPostsubmit }
+ fun isClientPrevious(isClientPrevious: Boolean) = apply {
+ this.isClientPrevious = isClientPrevious
+ }
+ fun isServicePrevious(isServicePrevious: Boolean) = apply {
+ this.isServicePrevious = isServicePrevious
+ }
+ fun minSdk(minSdk: String) = apply { this.minSdk = minSdk }
+ fun serviceApkName(serviceApkName: String) = apply { this.serviceApkName = serviceApkName }
+ fun serviceApplicationId(serviceApplicationId: String) =
+ apply { this.serviceApplicationId = serviceApplicationId }
+ fun tag(tag: String) = apply { this.tag = tag }
+ fun testRunner(testRunner: String) = apply { this.testRunner = testRunner }
+
+ private fun mediaInstrumentationArgs(): String {
+ return if (isClientPrevious) {
+ if (isServicePrevious) {
+ CLIENT_PREVIOUS + SERVICE_PREVIOUS
+ } else {
+ CLIENT_PREVIOUS + SERVICE_TOT
+ }
+ } else {
+ if (isServicePrevious) {
+ CLIENT_TOT + SERVICE_PREVIOUS
+ } else {
+ CLIENT_TOT + SERVICE_TOT
+ }
+ }
+ }
+
+ fun build(): String {
+ val sb = StringBuilder()
+ sb.append(XML_HEADER_AND_LICENSE)
+ .append(CONFIGURATION_OPEN)
+ .append(MIN_API_LEVEL_CONTROLLER_OBJECT.replace("MIN_SDK", minSdk))
+ .append(TEST_SUITE_TAG_OPTION.replace("TEST_SUITE_TAG", tag))
+ .append(TEST_SUITE_TAG_OPTION.replace("TEST_SUITE_TAG", "media_compat"))
+ .append(
+ MODULE_METADATA_TAG_OPTION.replace(
+ "APPLICATION_ID", "$clientApplicationId;$serviceApplicationId"
+ )
+ )
+ .append(WIFI_DISABLE_OPTION)
+ .append(SETUP_INCLUDE)
+ .append(TARGET_PREPARER_OPEN)
+ .append(APK_INSTALL_OPTION.replace("APK_NAME", clientApkName))
+ .append(APK_INSTALL_OPTION.replace("APK_NAME", serviceApkName))
+ sb.append(TARGET_PREPARER_CLOSE)
+ .append(TEST_BLOCK_OPEN)
+ .append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
+ .append(PACKAGE_OPTION.replace("APPLICATION_ID", clientApplicationId))
+ .append(mediaInstrumentationArgs())
+ if (isPostsubmit)
+ sb.append(TEST_BLOCK_CLOSE)
+ .append(TEST_BLOCK_OPEN)
+ .append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
+ .append(PACKAGE_OPTION.replace("APPLICATION_ID", serviceApplicationId))
+ .append(mediaInstrumentationArgs())
+ .append(TEST_BLOCK_CLOSE)
+ else {
+ // add the small and medium test runners for both client and service apps
+ sb.append(SMALL_TEST_OPTIONS)
+ .append(TEST_BLOCK_CLOSE)
+ .append(TEST_BLOCK_OPEN)
+ .append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
+ .append(PACKAGE_OPTION.replace("APPLICATION_ID", clientApplicationId))
+ .append(mediaInstrumentationArgs())
+ .append(MEDIUM_TEST_OPTIONS)
+ .append(TEST_BLOCK_CLOSE)
+ .append(TEST_BLOCK_OPEN)
+ .append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
+ .append(PACKAGE_OPTION.replace("APPLICATION_ID", serviceApplicationId))
+ .append(mediaInstrumentationArgs())
+ .append(SMALL_TEST_OPTIONS)
+ .append(TEST_BLOCK_CLOSE)
+ .append(TEST_BLOCK_OPEN)
+ .append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
+ .append(PACKAGE_OPTION.replace("APPLICATION_ID", serviceApplicationId))
+ .append(mediaInstrumentationArgs())
+ .append(MEDIUM_TEST_OPTIONS)
+ .append(TEST_BLOCK_CLOSE)
+ }
+ sb.append(CONFIGURATION_CLOSE)
+ return sb.toString()
+ }
+}
+
+/**
+ * These constants are the building blocks of the xml configs, but
+ * they aren't very readable as separate chunks. Look to
+ * the golden examples at the bottom of
+ * {@link androidx.build.testConfiguration.XmlTestConfigVerificationTest}
+ * for examples of what the full xml will look like.
+ */
+
+private val XML_HEADER_AND_LICENSE = """
+ <?xml version="1.0" encoding="utf-8"?>
+ <!-- Copyright (C) 2020 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License")
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions
+ and limitations under the License.-->
+
+""".trimIndent()
+
+private val CONFIGURATION_OPEN = """
+ <configuration description="Runs tests for the module">
+
+""".trimIndent()
+
+private val CONFIGURATION_CLOSE = """
+ </configuration>
+""".trimIndent()
+
+private val MIN_API_LEVEL_CONTROLLER_OBJECT = """
+ <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MinApiLevelModuleController">
+ <option name="min-api-level" value="MIN_SDK" />
+ </object>
+
+""".trimIndent()
+
+private val TEST_SUITE_TAG_OPTION = """
+ <option name="test-suite-tag" value="TEST_SUITE_TAG" />
+
+""".trimIndent()
+
+private val MODULE_METADATA_TAG_OPTION = """
+ <option name="config-descriptor:metadata" key="applicationId" value="APPLICATION_ID" />
+
+""".trimIndent()
+
+private val WIFI_DISABLE_OPTION = """
+ <option name="wifi:disable" value="true" />
+
+""".trimIndent()
+
+private val SETUP_INCLUDE = """
+ <include name="google/unbundled/common/setup" />
+
+""".trimIndent()
+
+private val TARGET_PREPARER_OPEN = """
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+
+""".trimIndent()
+
+private val TARGET_PREPARER_CLOSE = """
+ </target_preparer>
+
+""".trimIndent()
+
+private val APK_INSTALL_OPTION = """
+ <option name="test-file-name" value="APK_NAME" />
+
+""".trimIndent()
+
+private val TEST_BLOCK_OPEN = """
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+
+""".trimIndent()
+
+private val TEST_BLOCK_CLOSE = """
+ </test>
+
+""".trimIndent()
+
+private val RUNNER_OPTION = """
+ <option name="runner" value="TEST_RUNNER"/>
+
+""".trimIndent()
+
+private val PACKAGE_OPTION = """
+ <option name="package" value="APPLICATION_ID" />
+
+""".trimIndent()
+
+private val BENCHMARK_PRESUBMIT_OPTION = """
+ <option name="instrumentation-arg" key="androidx.benchmark.dryRunMode.enable" value="true" />
+
+""".trimIndent()
+
+private val BENCHMARK_POSTSUBMIT_OPTIONS = """
+ <option name="instrumentation-arg" key="androidx.benchmark.output.enable" value="true" />
+ <option name="instrumentation-arg" key="listener" value="androidx.benchmark.junit4.InstrumentationResultsRunListener" />
+
+""".trimIndent()
+
+private val SMALL_TEST_OPTIONS = """
+ <option name="size" value="small" />
+ <option name="test-timeout" value="300" />
+
+""".trimIndent()
+
+private val MEDIUM_TEST_OPTIONS = """
+ <option name="size" value="medium" />
+ <option name="test-timeout" value="1500" />
+
+""".trimIndent()
+
+private val CLIENT_PREVIOUS = """
+ <option name="instrumentation-arg" key="client_version" value="previous" />
+
+""".trimIndent()
+
+private val CLIENT_TOT = """
+ <option name="instrumentation-arg" key="client_version" value="tot" />
+
+""".trimIndent()
+
+private val SERVICE_PREVIOUS = """
+ <option name="instrumentation-arg" key="service_version" value="previous" />
+
+""".trimIndent()
+
+private val SERVICE_TOT = """
+ <option name="instrumentation-arg" key="service_version" value="tot" />
+
+""".trimIndent()
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateMediaTestConfigurationTask.kt b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateMediaTestConfigurationTask.kt
new file mode 100644
index 0000000..8ef93bcb
--- /dev/null
+++ b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateMediaTestConfigurationTask.kt
@@ -0,0 +1,177 @@
+/*
+ * 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.build.testConfiguration
+
+import androidx.build.dependencyTracker.ProjectSubset
+import androidx.build.renameApkForTesting
+import com.android.build.api.variant.BuiltArtifacts
+import com.android.build.api.variant.BuiltArtifactsLoader
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+
+/**
+ * Writes three configuration files to test combinations of media client & service in
+ * <a href=https://source.android.com/devices/tech/test_infra/tradefed/testing/through-suite/android-test-structure>AndroidTest.xml</a>
+ * format that gets zipped alongside the APKs to be tested. The combinations are of previous and
+ * tip-of-tree versions client and service. We want to test every possible pairing that includes
+ * tip-of-tree.
+ *
+ * This config gets ingested by Tradefed.
+ */
+abstract class GenerateMediaTestConfigurationTask : DefaultTask() {
+
+ @get:InputFiles
+ abstract val clientToTFolder: DirectoryProperty
+
+ @get:Internal
+ abstract val clientToTLoader: Property<BuiltArtifactsLoader>
+
+ @get:InputFiles
+ abstract val clientPreviousFolder: DirectoryProperty
+
+ @get:Internal
+ abstract val clientPreviousLoader: Property<BuiltArtifactsLoader>
+
+ @get:InputFiles
+ abstract val serviceToTFolder: DirectoryProperty
+
+ @get:Internal
+ abstract val serviceToTLoader: Property<BuiltArtifactsLoader>
+
+ @get:InputFiles
+ abstract val servicePreviousFolder: DirectoryProperty
+
+ @get:Internal
+ abstract val servicePreviousLoader: Property<BuiltArtifactsLoader>
+
+ @get:Input
+ abstract val affectedModuleDetectorSubset: Property<ProjectSubset>
+
+ @get:Input
+ abstract val clientToTPath: Property<String>
+
+ @get:Input
+ abstract val clientPreviousPath: Property<String>
+
+ @get:Input
+ abstract val serviceToTPath: Property<String>
+
+ @get:Input
+ abstract val servicePreviousPath: Property<String>
+
+ @get:Input
+ abstract val minSdk: Property<Int>
+
+ @get:Input
+ abstract val testRunner: Property<String>
+
+ @get:OutputFile
+ abstract val clientPreviousServiceToT: RegularFileProperty
+
+ @get:OutputFile
+ abstract val clientToTServicePrevious: RegularFileProperty
+
+ @get:OutputFile
+ abstract val clientToTServiceToT: RegularFileProperty
+
+ @TaskAction
+ fun generateAndroidTestZip() {
+ val clientToTApk = resolveApk(clientToTFolder, clientToTLoader)
+ val clientPreviousApk = resolveApk(clientPreviousFolder, clientPreviousLoader)
+ val serviceToTApk = resolveApk(serviceToTFolder, serviceToTLoader)
+ val servicePreviousApk = resolveApk(
+ servicePreviousFolder, servicePreviousLoader
+ )
+ writeConfigFileContent(
+ clientToTApk, serviceToTApk, clientToTPath.get(),
+ serviceToTPath.get(), clientToTServiceToT, false, false
+ )
+ writeConfigFileContent(
+ clientToTApk, servicePreviousApk, clientToTPath.get(),
+ servicePreviousPath.get(), clientToTServicePrevious, false, true
+ )
+ writeConfigFileContent(
+ clientPreviousApk, serviceToTApk, clientPreviousPath.get(),
+ serviceToTPath.get(), clientPreviousServiceToT, true, false
+ )
+ }
+
+ private fun resolveApk(
+ apkFolder: DirectoryProperty,
+ apkLoader: Property<BuiltArtifactsLoader>
+ ): BuiltArtifacts {
+ return apkLoader.get().load(apkFolder.get())
+ ?: throw RuntimeException("Cannot load required APK for task: $name")
+ }
+
+ private fun resolveName(apk: BuiltArtifacts, path: String): String {
+ return apk.elements.single().outputFile.substringAfterLast("/")
+ .renameApkForTesting(path, false)
+ }
+
+ private fun writeConfigFileContent(
+ clientApk: BuiltArtifacts,
+ serviceApk: BuiltArtifacts,
+ clientPath: String,
+ servicePath: String,
+ outputFile: RegularFileProperty,
+ isClientPrevious: Boolean,
+ isServicePrevious: Boolean
+ ) {
+ val configBuilder = MediaConfigBuilder()
+ configBuilder.clientApkName(resolveName(clientApk, clientPath))
+ .clientApplicationId(clientApk.applicationId)
+ .serviceApkName(resolveName(serviceApk, servicePath))
+ .serviceApplicationId(serviceApk.applicationId)
+ .minSdk(minSdk.get().toString())
+ .testRunner(testRunner.get())
+ .isClientPrevious(isClientPrevious)
+ .isServicePrevious(isServicePrevious)
+ when (affectedModuleDetectorSubset.get()) {
+ ProjectSubset.CHANGED_PROJECTS, ProjectSubset.ALL_AFFECTED_PROJECTS -> {
+ configBuilder.isPostsubmit(true)
+ }
+ ProjectSubset.DEPENDENT_PROJECTS -> {
+ configBuilder.isPostsubmit(false)
+ }
+ else -> {
+ throw IllegalStateException(
+ "$name should not be running if the AffectedModuleDetector is returning " +
+ "${affectedModuleDetectorSubset.get()} for this project."
+ )
+ }
+ }
+
+ val resolvedOutputFile: File = outputFile.asFile.get()
+ if (!resolvedOutputFile.exists()) {
+ if (!resolvedOutputFile.createNewFile()) {
+ throw RuntimeException(
+ "Failed to create test configuration file: $outputFile"
+ )
+ }
+ }
+ resolvedOutputFile.writeText(configBuilder.build())
+ }
+}
diff --git a/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
new file mode 100644
index 0000000..a95e43c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
@@ -0,0 +1,136 @@
+/*
+ * 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.build.testConfiguration
+
+import androidx.build.dependencyTracker.ProjectSubset
+import androidx.build.renameApkForTesting
+import com.android.build.api.variant.BuiltArtifactsLoader
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+
+/**
+ * Writes a configuration file in
+ * <a href=https://source.android.com/devices/tech/test_infra/tradefed/testing/through-suite/android-test-structure>AndroidTest.xml</a>
+ * format that gets zipped alongside the APKs to be tested.
+ * This config gets ingested by Tradefed.
+ */
+abstract class GenerateTestConfigurationTask : DefaultTask() {
+
+ @get:InputFiles
+ @get:Optional
+ abstract val appFolder: DirectoryProperty
+
+ @get:Internal
+ abstract val appLoader: Property<BuiltArtifactsLoader>
+
+ @get:InputFiles
+ abstract val testFolder: DirectoryProperty
+
+ @get:Internal
+ abstract val testLoader: Property<BuiltArtifactsLoader>
+
+ @get:Input
+ abstract val minSdk: Property<Int>
+
+ @get:Input
+ abstract val hasBenchmarkPlugin: Property<Boolean>
+
+ @get:Input
+ abstract val testRunner: Property<String>
+
+ @get:Input
+ abstract val projectPath: Property<String>
+
+ @get:Input
+ abstract val affectedModuleDetectorSubset: Property<ProjectSubset>
+
+ @get:OutputFile
+ abstract val outputXml: RegularFileProperty
+
+ @TaskAction
+ fun generateAndroidTestZip() {
+ writeConfigFileContent()
+ }
+
+ private fun writeConfigFileContent() {
+ /*
+ Testing an Android Application project involves 2 APKS: an application to be instrumented,
+ and a test APK. Testing an Android Library project involves only 1 APK, since the library
+ is bundled inside the test APK, meaning it is self instrumenting. We add extra data to
+ configurations testing Android Application projects, so that both APKs get installed.
+ */
+ val configBuilder = ConfigBuilder()
+ if (appLoader.isPresent) {
+ val appApk = appLoader.get().load(appFolder.get())
+ ?: throw RuntimeException("Cannot load required APK for task: $name")
+ val appName = appApk.elements.single().outputFile.substringAfterLast("/")
+ .renameApkForTesting(projectPath.get(), hasBenchmarkPlugin.get())
+ configBuilder.appApkName(appName)
+ }
+ val isPostsubmit: Boolean = when (affectedModuleDetectorSubset.get()) {
+ ProjectSubset.CHANGED_PROJECTS, ProjectSubset.ALL_AFFECTED_PROJECTS -> {
+ true
+ }
+ ProjectSubset.DEPENDENT_PROJECTS -> {
+ false
+ }
+ else -> {
+ throw IllegalStateException(
+ "$name should not be running if the AffectedModuleDetector is returning " +
+ "${affectedModuleDetectorSubset.get()} for this project."
+ )
+ }
+ }
+ configBuilder.isPostsubmit(isPostsubmit)
+ if (hasBenchmarkPlugin.get()) {
+ configBuilder.isBenchmark(true)
+ if (isPostsubmit) {
+ configBuilder.tag("microbenchmarks")
+ }
+ } else if (projectPath.get().endsWith("macrobenchmark")) {
+ configBuilder.tag("macrobenchmarks")
+ }
+ val testApk = testLoader.get().load(testFolder.get())
+ ?: throw RuntimeException("Cannot load required APK for task: $name")
+ val testName = testApk.elements.single().outputFile
+ .substringAfterLast("/")
+ .renameApkForTesting(projectPath.get(), hasBenchmarkPlugin.get())
+ configBuilder.testApkName(testName)
+ .applicationId(testApk.applicationId)
+ .minSdk(minSdk.get().toString())
+ .testRunner(testRunner.get())
+
+ val resolvedOutputFile: File = outputXml.asFile.get()
+ if (!resolvedOutputFile.exists()) {
+ if (!resolvedOutputFile.createNewFile()) {
+ throw RuntimeException(
+ "Failed to create test configuration file: $outputXml"
+ )
+ }
+ }
+ resolvedOutputFile.writeText(configBuilder.build())
+ }
+}
diff --git a/buildSrc/src/main/kotlin/androidx/build/TestSuiteConfiguration.kt b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
similarity index 92%
rename from buildSrc/src/main/kotlin/androidx/build/TestSuiteConfiguration.kt
rename to buildSrc/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index c676f20..764abcc 100644
--- a/buildSrc/src/main/kotlin/androidx/build/TestSuiteConfiguration.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -16,10 +16,15 @@
@file:Suppress("UnstableApiUsage") // Incubating AGP APIs
-package androidx.build
+package androidx.build.testConfiguration
+import androidx.build.AndroidXPlugin
+import androidx.build.asFilenamePrefix
import androidx.build.dependencyTracker.AffectedModuleDetector
+import androidx.build.getTestConfigDirectory
import androidx.build.gradle.getByType
+import androidx.build.hasAndroidTestSourceCode
+import androidx.build.hasBenchmarkPlugin
import com.android.build.api.artifact.ArtifactType
import com.android.build.api.artifact.Artifacts
import com.android.build.api.extension.AndroidComponentsExtension
@@ -85,8 +90,7 @@
if (!project.parent!!.tasks.withType(GenerateMediaTestConfigurationTask::class.java)
.names.contains(
"support-$mediaPrefix-test${
- AndroidXPlugin
- .GENERATE_TEST_CONFIGURATION_TASK
+ AndroidXPlugin.GENERATE_TEST_CONFIGURATION_TASK
}"
)
) {
@@ -95,6 +99,11 @@
GenerateMediaTestConfigurationTask::class.java
) { task ->
AffectedModuleDetector.configureTaskGuard(task)
+ task.affectedModuleDetectorSubset.set(
+ project.provider {
+ AffectedModuleDetector.getProjectSubset(project)
+ }
+ )
}
project.rootProject.tasks.findByName(AndroidXPlugin.ZIP_TEST_CONFIGS_WITH_APKS_TASK)!!
.dependsOn(task)
@@ -103,8 +112,7 @@
return project.parent!!.tasks.withType(GenerateMediaTestConfigurationTask::class.java)
.named(
"support-$mediaPrefix-test${
- AndroidXPlugin
- .GENERATE_TEST_CONFIGURATION_TASK
+ AndroidXPlugin.GENERATE_TEST_CONFIGURATION_TASK
}"
)
}
@@ -203,4 +211,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/busytown/impl/build.sh b/busytown/impl/build.sh
index 67bb30c..09fac0b 100755
--- a/busytown/impl/build.sh
+++ b/busytown/impl/build.sh
@@ -6,14 +6,18 @@
# find script
SCRIPT_DIR="$(cd $(dirname $0) && pwd)"
-# resolve DIST_DIR
+# resolve directories
if [ "$DIST_DIR" == "" ]; then
DIST_DIR="$SCRIPT_DIR/../../../../out/dist"
fi
mkdir -p "$DIST_DIR"
-
-# cd to checkout root
cd "$SCRIPT_DIR/../../../.."
+OUT_DIR="$PWD/out"
+mkdir -p "$OUT_DIR"
+
+# record the build start time
+BUILD_START_MARKER="$OUT_DIR/build.sh.start"
+touch $BUILD_START_MARKER
# runs a given command and prints its result if it fails
function run() {
@@ -37,9 +41,12 @@
fi
# --no-watch-fs disables file system watch, because it does not work on busytown
# due to our builders using OS that is too old.
-run $PROJECTS_ARG OUT_DIR=out DIST_DIR=$DIST_DIR ANDROID_HOME=./prebuilts/fullsdk-linux \
+run $PROJECTS_ARG OUT_DIR=$OUT_DIR DIST_DIR=$DIST_DIR ANDROID_HOME=./prebuilts/fullsdk-linux \
frameworks/support/gradlew -p frameworks/support \
--stacktrace \
-Pandroidx.summarizeStderr \
--no-watch-fs \
"$@"
+
+# check that no unexpected modifications were made to the source repository, such as new cache directories
+$SCRIPT_DIR/verify_no_caches_in_source_repo.sh $BUILD_START_MARKER
diff --git a/busytown/impl/verify_no_caches_in_source_repo.sh b/busytown/impl/verify_no_caches_in_source_repo.sh
new file mode 100755
index 0000000..7e5e028
--- /dev/null
+++ b/busytown/impl/verify_no_caches_in_source_repo.sh
@@ -0,0 +1,69 @@
+#!/bin/bash
+set -e
+
+function usage() {
+ echo "Confirms that no unexpected, generated files exist in the source repository"
+ echo
+ echo "Usage: $0 <timestamp file>"
+ echo
+ echo "<timestamp file>: any file newer than this one will be considered an error unless it is already exempted"
+ return 1
+}
+
+# parse arguments
+# a file whose timestamp is the oldest acceptable timestamp for source files
+COMPARE_TO_FILE="$1"
+if [ "$COMPARE_TO_FILE" == "" ]; then
+ usage
+fi
+
+# get script path
+SCRIPT_DIR="$(cd $(dirname $0) && pwd)"
+SOURCE_DIR="$(cd $SCRIPT_DIR/../.. && pwd)"
+
+# confirm that no files in the source repo were unexpectedly created (other than known exemptions)
+function checkForGeneratedFilesInSourceRepo() {
+
+ # Paths that are still expected to be generated and that we have to allow
+ # If you need add or remove an exemption here, update cleanBuild.sh too
+ EXEMPT_PATHS=".gradle appsearch/local-storage/.cxx buildSrc/.gradle local.properties"
+ # put "./" in front of each path to match the output from 'find'
+ EXEMPT_PATHS="$(echo " $EXEMPT_PATHS" | sed 's| | ./|g')"
+ # build a `find` argument for skipping descending into the exempt paths
+ EXEMPTIONS_ARGUMENT="$(echo $EXEMPT_PATHS | sed 's/ /\n/g' | sed 's|\(.*\)|-path \1 -prune -o|g' | xargs echo)"
+
+ # Search for files that were created or updated more recently than the build start.
+ # Unfortunately we can't also include directories because the `test` task seems to update
+ # the modification time in several projects
+ GENERATED_FILES="$(cd $SOURCE_DIR && find . $EXEMPTIONS_ARGUMENT -newer $COMPARE_TO_FILE -type f)"
+ UNEXPECTED_GENERATED_FILES=""
+ for f in $GENERATED_FILES; do
+ exempt=false
+ for exemption in $EXEMPT_PATHS; do
+ if [ "$f" == "$exemption" ]; then
+ exempt=true
+ break
+ fi
+ if [ "$f" == "$(dirname $exemption)" ]; then
+ # When the exempt directory gets created, its parent dir will be modified
+ # So, we ignore changes to the parent dir too (but not necessarily changes in sibling dirs)
+ exempt=true
+ break
+ fi
+ done
+ if [ "$exempt" == "false" ]; then
+ UNEXPECTED_GENERATED_FILES="$UNEXPECTED_GENERATED_FILES $f"
+ fi
+ done
+ if [ "$UNEXPECTED_GENERATED_FILES" != "" ]; then
+ echo >&2
+ echo "Unexpectedly found these files generated or modified by the build:
+
+${UNEXPECTED_GENERATED_FILES}
+
+Generated files should go in OUT_DIR instead because that is where developers expect to find them
+(to make it easier to diagnose build problems: inspect or delete these files)" >&2
+ exit 1
+ fi
+}
+checkForGeneratedFilesInSourceRepo
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
index 442db04..1ad9830 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
@@ -21,6 +21,7 @@
import android.view.Surface
import androidx.camera.camera2.pipe.CameraMetadata
import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.impl.Log
import androidx.camera.camera2.pipe.integration.config.CameraConfig
import androidx.camera.camera2.pipe.integration.config.CameraScope
import androidx.camera.camera2.pipe.integration.impl.CameraCallbackMap
@@ -30,12 +31,15 @@
import androidx.camera.core.ZoomState
import androidx.camera.core.impl.CameraCaptureCallback
import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.Quirks
import androidx.camera.core.impl.utils.CameraOrientationUtil
import androidx.lifecycle.LiveData
import java.util.concurrent.Executor
import javax.inject.Inject
import javax.inject.Provider
+internal val defaultQuirks = Quirks(emptyList())
+
/**
* Adapt the [CameraInfoInternal] interface to [CameraPipe].
*/
@@ -88,4 +92,9 @@
override fun getImplementationType(): String = "CameraPipe"
override fun toString(): String = "CameraInfoAdapter<$cameraConfig.cameraId>"
+
+ override fun getCameraQuirks(): Quirks {
+ Log.warn { "TODO: Quirks are not yet supported." }
+ return defaultQuirks
+ }
}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
index 3174c2c..d3dcff7 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
@@ -28,13 +28,11 @@
import androidx.camera.core.impl.CameraInternal
import androidx.camera.core.impl.LiveDataObservable
import androidx.camera.core.impl.Observable
-import androidx.camera.core.impl.Quirks
import androidx.camera.core.impl.utils.futures.Futures
import com.google.common.util.concurrent.ListenableFuture
import kotlinx.atomicfu.atomic
import javax.inject.Inject
-internal val defaultQuirks = Quirks(emptyList())
internal val cameraAdapterIds = atomic(0)
/**
@@ -58,11 +56,6 @@
// TODO: Consider preloading the list of camera ids and metadata.
}
- override fun getCameraQuirks(): Quirks {
- warn { "TODO: Quirks are not yet supported." }
- return defaultQuirks
- }
-
// Load / unload methods
override fun open() {
debug { "$this#open" }
diff --git a/camera/camera-camera2-pipe/dependencies.gradle b/camera/camera-camera2-pipe/dependencies.gradle
index 6cd62b7..db00727 100644
--- a/camera/camera-camera2-pipe/dependencies.gradle
+++ b/camera/camera-camera2-pipe/dependencies.gradle
@@ -22,7 +22,7 @@
"androidx.annotation:annotation:1.0.0",
KOTLIN_STDLIB,
KOTLIN_COROUTINES_ANDROID,
- "org.jetbrains.kotlinx:atomicfu:0.13.1"
+ "org.jetbrains.kotlinx:atomicfu:0.15.0"
],
IMPLEMENTATION : [DAGGER]
]
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
index 1dddd47..c797ab9 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
@@ -95,7 +95,8 @@
companion object Constants3A {
// Constants related to controlling the time or frame budget a 3A operation should get.
const val DEFAULT_FRAME_LIMIT = 60
- const val DEFAULT_TIME_LIMIT_MS = 3000
+ const val DEFAULT_TIME_LIMIT_MS = 3_000
+ const val DEFAULT_TIME_LIMIT_NS = 3_000_000_000L
// Constants related to metering regions.
/** No metering region is specified. */
@@ -170,19 +171,19 @@
*
* @param frameLimit the maximum number of frames to wait before we give up waiting for
* this operation to complete.
- * @param timeLimitMs the maximum time limit in ms we wait before we give up waiting for
+ * @param timeLimitNs the maximum time limit in ms we wait before we give up waiting for
* this operation to complete.
*
* @return [Result3A], which will contain the latest frame number at which the locks were
* applied or the frame number at which the method returned early because either frame limit
* or time limit was reached.
*/
- fun lock3A(
+ suspend fun lock3A(
aeLockBehavior: Lock3ABehavior? = null,
afLockBehavior: Lock3ABehavior? = null,
awbLockBehavior: Lock3ABehavior? = null,
frameLimit: Int = DEFAULT_FRAME_LIMIT,
- timeLimitMs: Int = DEFAULT_TIME_LIMIT_MS
+ timeLimitNs: Long = DEFAULT_TIME_LIMIT_NS
): Deferred<Result3A>
/**
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Lock3ABehavior.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Lock3ABehavior.kt
index 27406f7..5d594f7 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Lock3ABehavior.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Lock3ABehavior.kt
@@ -41,4 +41,36 @@
* Initiate a new scan, and then lock the values once the scan is done.
*/
AFTER_NEW_SCAN,
-}
\ No newline at end of file
+}
+
+fun Lock3ABehavior?.shouldUnlockAe() =
+ this == Lock3ABehavior.AFTER_NEW_SCAN
+
+fun Lock3ABehavior?.shouldUnlockAf() =
+ this == Lock3ABehavior.AFTER_NEW_SCAN
+
+fun Lock3ABehavior?.shouldUnlockAwb() =
+ this == Lock3ABehavior.AFTER_NEW_SCAN
+
+// For ae and awb if we set the lock = true in the capture request the camera device
+// locks them immediately. So when we want to wait for ae to converge we have to explicitly
+// wait for it to converge.
+fun Lock3ABehavior?.shouldWaitForAeToConverge() =
+ this != null && this != Lock3ABehavior.IMMEDIATE
+
+fun Lock3ABehavior?.shouldWaitForAwbToConverge() =
+ this != null && this != Lock3ABehavior.IMMEDIATE
+
+// TODO(sushilnath@): add the optimization to not wait for af to converge before sending the
+// trigger for modes other than CONTINUOUS_VIDEO. The paragraph below explains the reasoning.
+//
+// For af, if the mode is MACRO, AUTO or CONTINUOUS_PICTURE and we send a capture request to
+// start an af trigger then camera device starts a new scan(for AUTO mode) or waits for the
+// current scan to finish(for CONTINUOUS_PICTURE) and then locks the auto-focus, so if we want
+// to wait for af to converge before locking it, we don't have to explicitly wait for
+// convergence, we can send the trigger right away, but if the mode is CONTINUOUS_VIDEO then
+// sending a request to start a trigger locks the auto focus immediately, so if we want af to
+// converge first then we have to explicitly wait for it.
+// Ref: https://developer.android.com/reference/android/hardware/camera2/CaptureResult#CONTROL_AF_STATE
+fun Lock3ABehavior?.shouldWaitForAfToConverge() =
+ this != null && this != Lock3ABehavior.IMMEDIATE
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt
index 9d5c485..2b7aa32 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt
@@ -30,6 +30,7 @@
import kotlinx.coroutines.Deferred
internal val cameraGraphSessionIds = atomic(0)
+
class CameraGraphSessionImpl(
private val token: TokenLock.Token,
private val graphProcessor: GraphProcessor,
@@ -84,14 +85,20 @@
TODO("Implement setTorch")
}
- override fun lock3A(
+ override suspend fun lock3A(
aeLockBehavior: Lock3ABehavior?,
afLockBehavior: Lock3ABehavior?,
awbLockBehavior: Lock3ABehavior?,
frameLimit: Int,
- timeLimitMs: Int
+ timeLimitNs: Long
): Deferred<Result3A> {
- TODO("Implement lock3A")
+ // TODO(sushilnath): check if the device or the current mode supports lock for each of
+ // ae, af and awb respectively. If not supported return an exception or return early with
+ // the right status code.
+ return controller3A.lock3A(
+ aeLockBehavior, afLockBehavior, awbLockBehavior, frameLimit,
+ timeLimitNs
+ )
}
override fun lock3A(
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Controller3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Controller3A.kt
index df3b3c2..4de0475 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Controller3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Controller3A.kt
@@ -16,16 +16,28 @@
package androidx.camera.camera2.pipe.impl
+import android.hardware.camera2.CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+import android.hardware.camera2.CaptureRequest.CONTROL_AF_TRIGGER_START
import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureRequest.CONTROL_AF_TRIGGER
import android.hardware.camera2.CaptureResult
import android.hardware.camera2.params.MeteringRectangle
import androidx.annotation.GuardedBy
import androidx.camera.camera2.pipe.AeMode
import androidx.camera.camera2.pipe.AfMode
import androidx.camera.camera2.pipe.AwbMode
+import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraGraph.Constants3A.FRAME_NUMBER_INVALID
+import androidx.camera.camera2.pipe.Lock3ABehavior
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.Status3A
+import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.shouldUnlockAe
+import androidx.camera.camera2.pipe.shouldUnlockAf
+import androidx.camera.camera2.pipe.shouldUnlockAwb
+import androidx.camera.camera2.pipe.shouldWaitForAeToConverge
+import androidx.camera.camera2.pipe.shouldWaitForAfToConverge
+import androidx.camera.camera2.pipe.shouldWaitForAwbToConverge
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.cancel
@@ -38,6 +50,66 @@
private val graphState3A: GraphState3A,
private val graphListener3A: Listener3A
) {
+ companion object {
+ private val aeConvergedStateList = listOf(
+ CaptureResult.CONTROL_AE_STATE_CONVERGED,
+ CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED,
+ CaptureResult.CONTROL_AE_STATE_LOCKED
+ )
+
+ private val awbConvergedStateList = listOf(
+ CaptureResult.CONTROL_AWB_STATE_CONVERGED,
+ CaptureResult.CONTROL_AWB_STATE_LOCKED
+ )
+
+ private val afConvergedStateList = listOf(
+ CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
+ CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED,
+ CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+ CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED
+ )
+
+ private val aeLockedStateList = listOf(CaptureResult.CONTROL_AE_STATE_LOCKED)
+
+ private val awbLockedStateList = listOf(CaptureResult.CONTROL_AWB_STATE_LOCKED)
+
+ private val afLockedStateList = listOf(
+ CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+ CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED
+ )
+
+ val parameterForAfTriggerStart = mapOf<CaptureRequest.Key<*>, Any>(
+ CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_START
+ )
+
+ val parameterForAfTriggerCancel = mapOf<CaptureRequest.Key<*>, Any>(
+ CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_CANCEL
+ )
+
+ private val result3ASubmitFailed = Result3A(FRAME_NUMBER_INVALID, Status3A.SUBMIT_FAILED)
+
+ private val aeUnlockedStateList = listOf(
+ CaptureResult.CONTROL_AE_STATE_INACTIVE,
+ CaptureResult.CONTROL_AE_STATE_SEARCHING,
+ CaptureResult.CONTROL_AE_STATE_CONVERGED,
+ CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED
+ )
+
+ private val afUnlockedStateList = listOf(
+ CaptureResult.CONTROL_AF_STATE_INACTIVE,
+ CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN,
+ CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+ CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
+ CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED
+ )
+
+ private val awbUnlockedStateList = listOf(
+ CaptureResult.CONTROL_AWB_STATE_INACTIVE,
+ CaptureResult.CONTROL_AWB_STATE_SEARCHING,
+ CaptureResult.CONTROL_AWB_STATE_CONVERGED
+ )
+ }
+
// Keep track of the result associated with latest call to update3A. If update3A is called again
// and the current result is not complete, we will cancel the current result.
@GuardedBy("this")
@@ -59,7 +131,7 @@
// Update the 3A state of the graph. This will make sure then when GraphProcessor builds
// the next request it will apply the 3A parameters corresponding to the updated 3A state
// to the request.
- graphState3A.update(aeMode, afMode, awbMode, aeRegions, afRegions, awbRegions)
+ graphState3A.update(aeMode, afMode, awbMode, aeRegions, afRegions, awbRegions, null, null)
// Try submitting a new repeating request with the 3A parameters corresponding to the new
// 3A state and corresponding listeners.
graphProcessor.invalidate()
@@ -110,13 +182,261 @@
if (!graphProcessor.submit(extra3AParams)) {
graphListener3A.removeListener(listener)
- return CompletableDeferred(
- Result3A(FRAME_NUMBER_INVALID, Status3A.SUBMIT_FAILED)
- )
+ return CompletableDeferred(result3ASubmitFailed)
}
return listener.getDeferredResult()
}
+ /**
+ * Given the desired lock behaviors for ae, af and awb, this method, (a) first unlocks them and
+ * wait for them to converge, and then (b) locks them.
+ *
+ * (a) In this step, as needed, we first send a single request with 'af trigger = cancel' to
+ * unlock af, and then a repeating request to unlock ae and awb. We suspend till we receive a
+ * response from the camera that each of the ae, af awb are converged.
+ * (b) In this step, as needed, we submit a repeating request to lock ae and awb, and then a
+ * single request to lock af by setting 'af trigger = start'. Once these requests are submitted
+ * we don't wait further and immediately return a Deferred<Result3A> which gets completed when
+ * the capture result with correct lock states for ae, af and awb is received.
+ *
+ * If we received an error when submitting any of the above requests or if waiting for the
+ * desired 3A state times out then we return early with the appropriate status code.
+ *
+ * Note: the frameLimit and timeLimitNs applies to each of the above steps (a) and (b) and not
+ * as a whole for the whole lock3A method. Thus, in the worst case this method including the
+ * completion of returned Deferred<Result3A> can take 2 * min(time equivalent of frameLimit,
+ * timeLimit) to complete
+ */
+ suspend fun lock3A(
+ aeLockBehavior: Lock3ABehavior? = null,
+ afLockBehavior: Lock3ABehavior? = null,
+ awbLockBehavior: Lock3ABehavior? = null,
+ frameLimit: Int = CameraGraph.DEFAULT_FRAME_LIMIT,
+ timeLimitMsNs: Long? = CameraGraph.DEFAULT_TIME_LIMIT_NS
+ ): Deferred<Result3A> {
+ // If we explicitly need to unlock af first before proceeding to lock it, we need to send
+ // a single request with TRIGGER = TRIGGER_CANCEL so that af can start a fresh scan.
+ if (afLockBehavior.shouldUnlockAf()) {
+ debug { "lock3A - sending a request to unlock af first." }
+ if (!graphProcessor.submit(parameterForAfTriggerCancel)) {
+ return CompletableDeferred(result3ASubmitFailed)
+ }
+ }
+
+ // As needed unlock ae, awb and wait for ae, af and awb to converge.
+ if (aeLockBehavior.shouldWaitForAeToConverge() ||
+ afLockBehavior.shouldWaitForAfToConverge() ||
+ awbLockBehavior.shouldWaitForAwbToConverge()
+ ) {
+ val converged3AExitConditions = createConverged3AExitConditions(
+ aeLockBehavior.shouldWaitForAeToConverge(),
+ afLockBehavior.shouldWaitForAfToConverge(),
+ awbLockBehavior.shouldWaitForAwbToConverge()
+ )
+ val listener = Result3AStateListenerImpl(
+ converged3AExitConditions,
+ frameLimit,
+ timeLimitMsNs
+ )
+ graphListener3A.addListener(listener)
+
+ // If we have to explicitly unlock ae, awb, then update the 3A state of the camera
+ // graph. This is because ae, awb lock values should stay as part of repeating
+ // request to the camera device. For af we need only one single request to trigger it,
+ // leaving it unset in the subsequent requests to the camera device will not affect the
+ // previously sent af trigger.
+ val aeLockValue = if (aeLockBehavior.shouldUnlockAe()) false else null
+ val awbLockValue = if (awbLockBehavior.shouldUnlockAwb()) false else null
+ if (aeLockValue != null || awbLockValue != null) {
+ debug { "lock3A - setting aeLock=$aeLockValue, awbLock=$awbLockValue" }
+ graphState3A.update(
+ aeLock = aeLockValue,
+ awbLock = awbLockValue
+ )
+ }
+ graphProcessor.invalidate()
+
+ debug {
+ "lock3A - waiting for" +
+ (if (aeLockBehavior.shouldWaitForAeToConverge()) " ae" else "") +
+ (if (afLockBehavior.shouldWaitForAfToConverge()) " af" else "") +
+ (if (awbLockBehavior.shouldWaitForAwbToConverge()) " awb" else "") +
+ " to converge before locking them."
+ }
+ val result = listener.getDeferredResult().await()
+ debug {
+ "lock3A - converged at frame number=${result.frameNumber.value}, status=${result
+ .status}"
+ }
+ // Return immediately if we encounter an error when unlocking and waiting for
+ // convergence.
+ if (result.status != Status3A.OK) {
+ return CompletableDeferred(result)
+ }
+ }
+
+ return lock3ANow(aeLockBehavior, afLockBehavior, awbLockBehavior, frameLimit, timeLimitMsNs)
+ }
+
+ /**
+ * This method unlocks ae, af and awb, as specified by setting the corresponding parameter to
+ * true.
+ *
+ * There are two requests involved in this operation, (a) a single request with af trigger =
+ * cancel, to unlock af, and then (a) a repeating request to unlock ae, awb.
+ */
+ suspend fun unlock3A(
+ ae: Boolean? = null,
+ af: Boolean? = null,
+ awb: Boolean? = null
+ ): Deferred<Result3A> {
+ check(ae == true || af == true || awb == true) { "No parameter has value as true" }
+ // If we explicitly need to unlock af first before proceeding to lock it, we need to send
+ // a single request with TRIGGER = TRIGGER_CANCEL so that af can start a fresh scan.
+ if (af == true) {
+ debug { "unlock3A - sending a request to unlock af first." }
+ if (!graphProcessor.submit(parameterForAfTriggerCancel)) {
+ debug { "unlock3A - request to unlock af failed, returning early." }
+ return CompletableDeferred(result3ASubmitFailed)
+ }
+ }
+
+ // As needed unlock ae, awb and wait for ae, af and awb to converge.
+ val unlocked3AExitConditions = createUnLocked3AExitConditions(
+ ae == true,
+ af == true,
+ awb == true
+ )
+ val listener = Result3AStateListenerImpl(unlocked3AExitConditions)
+ graphListener3A.addListener(listener)
+
+ // Update the 3A state of the camera graph and invalidate the repeating request with the
+ // new state.
+ val aeLockValue = if (ae == true) false else null
+ val awbLockValue = if (awb == true) false else null
+ if (aeLockValue != null || awbLockValue != null) {
+ debug { "unlock3A - updating graph state, aeLock=$aeLockValue, awbLock=$awbLockValue" }
+ graphState3A.update(
+ aeLock = aeLockValue,
+ awbLock = awbLockValue
+ )
+ }
+ graphProcessor.invalidate()
+ return listener.getDeferredResult()
+ }
+
+ private suspend fun lock3ANow(
+ aeLockBehavior: Lock3ABehavior?,
+ afLockBehavior: Lock3ABehavior?,
+ awbLockBehavior: Lock3ABehavior?,
+ frameLimit: Int?,
+ timeLimitMsNs: Long?
+ ): Deferred<Result3A> {
+ val finalAeLockValue = if (aeLockBehavior == null) null else true
+ val finalAwbLockValue = if (awbLockBehavior == null) null else true
+ val locked3AExitConditions = createLocked3AExitConditions(
+ finalAeLockValue != null,
+ afLockBehavior != null,
+ finalAwbLockValue != null
+ )
+
+ var resultForLocked: Deferred<Result3A>? = null
+ if (locked3AExitConditions.isNotEmpty()) {
+ val listener = Result3AStateListenerImpl(
+ locked3AExitConditions,
+ frameLimit,
+ timeLimitMsNs
+ )
+ graphListener3A.addListener(listener)
+ graphState3A.update(aeLock = finalAeLockValue, awbLock = finalAwbLockValue)
+ debug {
+ "lock3A - submitting request with aeLock=$finalAeLockValue , " +
+ "awbLock=$finalAwbLockValue"
+ }
+ graphProcessor.invalidate()
+ resultForLocked = listener.getDeferredResult()
+ }
+
+ if (afLockBehavior == null) {
+ return resultForLocked!!
+ }
+
+ debug { "lock3A - submitting a request to lock af." }
+ if (!graphProcessor.submit(parameterForAfTriggerStart)) {
+ // TODO(sushilnath@): Change the error code to a more specific code so it's clear
+ // that one of the request in sequence of requests failed and the caller should
+ // unlock 3A to bring the 3A system to an initial state and then try again if they
+ // want to. The other option is to reset or restore the 3A state here.
+ return CompletableDeferred(result3ASubmitFailed)
+ }
+ return resultForLocked!!
+ }
+
+ private fun createConverged3AExitConditions(
+ waitForAeToConverge: Boolean,
+ waitForAfToConverge: Boolean,
+ waitForAwbToConverge: Boolean
+ ): Map<CaptureResult.Key<*>, List<Any>> {
+ if (
+ !waitForAeToConverge && !waitForAfToConverge && !waitForAwbToConverge
+ ) {
+ return mapOf()
+ }
+ val exitConditionMapForConverged = mutableMapOf<CaptureResult.Key<*>, List<Any>>()
+ if (waitForAeToConverge) {
+ exitConditionMapForConverged[CaptureResult.CONTROL_AE_STATE] = aeConvergedStateList
+ }
+ if (waitForAwbToConverge) {
+ exitConditionMapForConverged[CaptureResult.CONTROL_AWB_STATE] = awbConvergedStateList
+ }
+ if (waitForAfToConverge) {
+ exitConditionMapForConverged[CaptureResult.CONTROL_AF_STATE] = afConvergedStateList
+ }
+ return exitConditionMapForConverged
+ }
+
+ private fun createLocked3AExitConditions(
+ waitForAeToLock: Boolean,
+ waitForAfToLock: Boolean,
+ waitForAwbToLock: Boolean
+ ): Map<CaptureResult.Key<*>, List<Any>> {
+ if (!waitForAeToLock && !waitForAfToLock && !waitForAwbToLock) {
+ return mapOf()
+ }
+ val exitConditionMapForLocked = mutableMapOf<CaptureResult.Key<*>, List<Any>>()
+ if (waitForAeToLock) {
+ exitConditionMapForLocked[CaptureResult.CONTROL_AE_STATE] = aeLockedStateList
+ }
+ if (waitForAfToLock) {
+ exitConditionMapForLocked[CaptureResult.CONTROL_AF_STATE] = afLockedStateList
+ }
+ if (waitForAwbToLock) {
+ exitConditionMapForLocked[CaptureResult.CONTROL_AWB_STATE] = awbLockedStateList
+ }
+ return exitConditionMapForLocked
+ }
+
+ private fun createUnLocked3AExitConditions(
+ ae: Boolean,
+ af: Boolean,
+ awb: Boolean
+ ): Map<CaptureResult.Key<*>, List<Any>> {
+ if (!ae && !af && !awb) {
+ return mapOf()
+ }
+ val exitConditionMapForUnLocked = mutableMapOf<CaptureResult.Key<*>, List<Any>>()
+ if (ae) {
+ exitConditionMapForUnLocked[CaptureResult.CONTROL_AE_STATE] = aeUnlockedStateList
+ }
+ if (af) {
+ exitConditionMapForUnLocked[CaptureResult.CONTROL_AF_STATE] = afUnlockedStateList
+ }
+ if (awb) {
+ exitConditionMapForUnLocked[CaptureResult.CONTROL_AWB_STATE] = awbUnlockedStateList
+ }
+ return exitConditionMapForUnLocked
+ }
+
// We create a map for the 3A modes and the desired values and leave out the keys
// corresponding to the metering regions. The reason being the camera framework can chose to
// crop or modify the metering regions as per its constraints. So when we receive at least
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState3A.kt
index b69476c..97664c0 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState3A.kt
@@ -28,6 +28,12 @@
*
* This object is used to maintain the key-value pairs for the most recent 3A state that is used
* when building the requests that are sent to a CameraCaptureSession.
+ *
+ * The state is comprised of the modes, metering regions for ae, af and awb, and locks for ae and
+ * awb. We don't track the lock for af since af lock is achieved by setting 'af trigger = start' in
+ * in a request and then omitting the af trigger field in the subsequent requests doesn't disturb
+ * the af state. However for ae and awb, the lock type is boolean and should be explicitly set to
+ * 'true' in the subsequent requests once we have locked ae/awb and want them to stay locked.
*/
@CameraGraphScope
class GraphState3A @Inject constructor() {
@@ -37,14 +43,18 @@
private var aeRegions: List<MeteringRectangle>? = null
private var afRegions: List<MeteringRectangle>? = null
private var awbRegions: List<MeteringRectangle>? = null
+ private var aeLock: Boolean? = null
+ private var awbLock: Boolean? = null
fun update(
- aeMode: AeMode?,
- afMode: AfMode?,
- awbMode: AwbMode?,
- aeRegions: List<MeteringRectangle>?,
- afRegions: List<MeteringRectangle>?,
- awbRegions: List<MeteringRectangle>?
+ aeMode: AeMode? = null,
+ afMode: AfMode? = null,
+ awbMode: AwbMode? = null,
+ aeRegions: List<MeteringRectangle>? = null,
+ afRegions: List<MeteringRectangle>? = null,
+ awbRegions: List<MeteringRectangle>? = null,
+ aeLock: Boolean? = null,
+ awbLock: Boolean? = null
) {
synchronized(this) {
aeMode?.let { this.aeMode = it }
@@ -53,6 +63,8 @@
aeRegions?.let { this.aeRegions = it }
afRegions?.let { this.afRegions = it }
awbRegions?.let { this.awbRegions = it }
+ aeLock?.let { this.aeLock = it }
+ awbLock?.let { this.awbLock = it }
}
}
@@ -65,6 +77,8 @@
aeRegions?.let { map.put(CaptureRequest.CONTROL_AE_REGIONS, it.toTypedArray()) }
afRegions?.let { map.put(CaptureRequest.CONTROL_AF_REGIONS, it.toTypedArray()) }
awbRegions?.let { map.put(CaptureRequest.CONTROL_AWB_REGIONS, it.toTypedArray()) }
+ aeLock?.let { map.put(CaptureRequest.CONTROL_AE_LOCK, it) }
+ awbLock?.let { map.put(CaptureRequest.CONTROL_AWB_LOCK, it) }
return map
}
}
@@ -77,6 +91,8 @@
aeRegions?.let { builder.set(CaptureRequest.CONTROL_AE_REGIONS, it.toTypedArray()) }
afRegions?.let { builder.set(CaptureRequest.CONTROL_AF_REGIONS, it.toTypedArray()) }
awbRegions?.let { builder.set(CaptureRequest.CONTROL_AWB_REGIONS, it.toTypedArray()) }
+ aeLock?.let { builder.set(CaptureRequest.CONTROL_AE_LOCK, it) }
+ awbLock?.let { builder.set(CaptureRequest.CONTROL_AWB_LOCK, it) }
}
}
}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ALock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ALock3ATest.kt
new file mode 100644
index 0000000..8d837e7
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ALock3ATest.kt
@@ -0,0 +1,641 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.impl
+
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.os.Build
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Lock3ABehavior
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestNumber
+import androidx.camera.camera2.pipe.Status3A
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
+import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
+import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
+import androidx.camera.camera2.pipe.testing.FakeRequestProcessor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+
+@RunWith(CameraPipeRobolectricTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class Controller3ALock3ATest {
+ private val graphProcessor = FakeGraphProcessor()
+ private val graphState3A = GraphState3A()
+ private val requestProcessor = FakeRequestProcessor(graphState3A)
+ private val listener3A = Listener3A()
+ private val controller3A = Controller3A(graphProcessor, graphState3A, listener3A)
+
+ @Test
+ fun testAfImmediateAeImmediate(): Unit = runBlocking {
+ initGraphProcessor()
+
+ val result = controller3A.lock3A(
+ afLockBehavior = Lock3ABehavior.IMMEDIATE,
+ aeLockBehavior = Lock3ABehavior.IMMEDIATE
+ )
+ assertThat(result.isCompleted).isFalse()
+
+ // Since requirement of to lock both AE and AF immediately, the requests to lock AE and AF
+ // are sent right away. The result of lock3A call will complete once AE and AF have reached
+ // their desired states. In this response i.e cameraResponse1, AF is still scanning so the
+ // result won't be complete.
+ val cameraResponse = GlobalScope.async {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_STATE to CaptureResult
+ .CONTROL_AF_STATE_PASSIVE_SCAN,
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+ )
+ )
+ )
+ }
+
+ cameraResponse.await()
+ assertThat(result.isCompleted).isFalse()
+
+ // One we we are notified that the AE and AF are in locked state, the result of lock3A call
+ // will complete.
+ GlobalScope.launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_STATE to CaptureResult
+ .CONTROL_AF_STATE_FOCUSED_LOCKED,
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+ )
+ )
+ )
+ }
+
+ val result3A = result.await()
+ assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.status).isEqualTo(Status3A.OK)
+
+ // We not check if the correct sequence of requests were submitted by lock3A call. The
+ // request should be a repeating request to lock AE.
+ val request1 = requestProcessor.nextEvent().request
+ assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ true
+ )
+
+ // The second request should be a single request to lock AF.
+ val request2 = requestProcessor.nextEvent().request
+ assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
+ assertThat(request2.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ true
+ )
+ }
+
+ @Test
+ fun testAfImmediateAeAfterCurrentScan(): Unit = runBlocking {
+ initGraphProcessor()
+
+ val lock3AAsyncTask = GlobalScope.async {
+ controller3A.lock3A(
+ afLockBehavior = Lock3ABehavior.IMMEDIATE,
+ aeLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN
+ )
+ }
+ assertThat(lock3AAsyncTask.isCompleted).isFalse()
+ // Launch a task to repeatedly invoke a given capture result.
+ GlobalScope.launch {
+ while (true) {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_STATE to
+ CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+ CaptureResult.CONTROL_AE_STATE to
+ CaptureResult.CONTROL_AE_STATE_CONVERGED
+ )
+ )
+ )
+ delay(Companion.FRAME_RATE_MS)
+ }
+ }
+
+ val result = lock3AAsyncTask.await()
+ // Result of lock3A call shouldn't be complete yet since the AE and AF are not locked yet.
+ assertThat(result.isCompleted).isFalse()
+
+ // Check the correctness of the requests submitted by lock3A.
+ // One repeating request was sent to monitor the state of AE to get converged.
+ requestProcessor.nextEvent().request
+ // Once AE is converged, another repeatingrequest is sent to lock AE.
+ val request1 = requestProcessor.nextEvent().request
+ assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ true
+ )
+
+ GlobalScope.launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_STATE to CaptureResult
+ .CONTROL_AF_STATE_FOCUSED_LOCKED,
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+ )
+ )
+ )
+ }
+
+ val result3A = result.await()
+ assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.status).isEqualTo(Status3A.OK)
+
+ // A single request to lock AF must have been used as well.
+ val request2 = requestProcessor.nextEvent().request
+ assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
+ }
+
+ @Test
+ fun testAfImmediateAeAfterNewScan(): Unit = runBlocking {
+ initGraphProcessor()
+
+ val lock3AAsyncTask = GlobalScope.async {
+ controller3A.lock3A(
+ afLockBehavior = Lock3ABehavior.IMMEDIATE,
+ aeLockBehavior = Lock3ABehavior.AFTER_NEW_SCAN
+ )
+ }
+ assertThat(lock3AAsyncTask.isCompleted).isFalse()
+ GlobalScope.launch {
+ while (true) {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_STATE to
+ CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+ CaptureResult.CONTROL_AE_STATE to
+ CaptureResult.CONTROL_AE_STATE_CONVERGED
+ )
+ )
+ )
+ delay(FRAME_RATE_MS)
+ }
+ }
+
+ val result = lock3AAsyncTask.await()
+ assertThat(result.isCompleted).isFalse()
+
+ // For a new AE scan we first send a request to unlock AE just in case it was
+ // previously or internally locked.
+ val request1 = requestProcessor.nextEvent().request
+ assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ false
+ )
+
+ GlobalScope.launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_STATE to CaptureResult
+ .CONTROL_AF_STATE_FOCUSED_LOCKED,
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+ )
+ )
+ )
+ }
+
+ val result3A = result.await()
+ assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.status).isEqualTo(Status3A.OK)
+
+ // There should be one more request to lock AE after new scan is done.
+ val request2 = requestProcessor.nextEvent().request
+ assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ true
+ )
+
+ // And one request to lock AF.
+ val request3 = requestProcessor.nextEvent().request
+ assertThat(request3!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
+ assertThat(request3.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ true
+ )
+ }
+
+ @Test
+ fun testAfAfterCurrentScanAeImmediate(): Unit = runBlocking {
+ initGraphProcessor()
+
+ val lock3AAsyncTask = GlobalScope.async {
+ controller3A.lock3A(
+ afLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN,
+ aeLockBehavior = Lock3ABehavior.IMMEDIATE
+ )
+ }
+ assertThat(lock3AAsyncTask.isCompleted).isFalse()
+ GlobalScope.launch {
+ while (true) {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_STATE to
+ CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
+ CaptureResult.CONTROL_AE_STATE to
+ CaptureResult.CONTROL_AE_STATE_CONVERGED
+ )
+ )
+ )
+ delay(FRAME_RATE_MS)
+ }
+ }
+
+ val result = lock3AAsyncTask.await()
+ assertThat(result.isCompleted).isFalse()
+
+ GlobalScope.launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_STATE to CaptureResult
+ .CONTROL_AF_STATE_FOCUSED_LOCKED,
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+ )
+ )
+ )
+ }
+
+ val result3A = result.await()
+ assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.status).isEqualTo(Status3A.OK)
+
+ // There should be one request to monitor AF to finish it's scan.
+ requestProcessor.nextEvent()
+ // One request to lock AE
+ val request2 = requestProcessor.nextEvent().request
+ assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ true
+ )
+
+ // And one request to lock AF.
+ val request3 = requestProcessor.nextEvent().request
+ assertThat(request3!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
+ assertThat(request3.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ true
+ )
+ }
+
+ @Test
+ fun testAfAfterNewScanScanAeImmediate(): Unit = runBlocking {
+ initGraphProcessor()
+
+ val lock3AAsyncTask = GlobalScope.async {
+ controller3A.lock3A(
+ afLockBehavior = Lock3ABehavior.AFTER_NEW_SCAN,
+ aeLockBehavior = Lock3ABehavior.IMMEDIATE
+ )
+ }
+ assertThat(lock3AAsyncTask.isCompleted).isFalse()
+ GlobalScope.launch {
+ while (true) {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_STATE to
+ CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
+ CaptureResult.CONTROL_AE_STATE to
+ CaptureResult.CONTROL_AE_STATE_CONVERGED
+ )
+ )
+ )
+ delay(FRAME_RATE_MS)
+ }
+ }
+
+ val result = lock3AAsyncTask.await()
+ assertThat(result.isCompleted).isFalse()
+
+ GlobalScope.launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_STATE to CaptureResult
+ .CONTROL_AF_STATE_FOCUSED_LOCKED,
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+ )
+ )
+ )
+ }
+
+ val result3A = result.await()
+ assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.status).isEqualTo(Status3A.OK)
+
+ // One request to cancel AF to start a new scan.
+ val request1 = requestProcessor.nextEvent().request
+ assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
+ // There should be one request to monitor AF to finish it's scan.
+ requestProcessor.nextEvent()
+
+ // There should be one request to monitor lock AE.
+ val request2 = requestProcessor.nextEvent().request
+ assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ true
+ )
+
+ // And one request to lock AF.
+ val request3 = requestProcessor.nextEvent().request
+ assertThat(request3!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
+ assertThat(request3.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ true
+ )
+ }
+
+ @Test
+ fun testAfAfterCurrentScanAeAfterCurrentScan(): Unit = runBlocking {
+ initGraphProcessor()
+
+ val lock3AAsyncTask = GlobalScope.async {
+ controller3A.lock3A(
+ afLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN,
+ aeLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN
+ )
+ }
+ assertThat(lock3AAsyncTask.isCompleted).isFalse()
+ GlobalScope.launch {
+ while (true) {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_STATE to
+ CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
+ CaptureResult.CONTROL_AE_STATE to
+ CaptureResult.CONTROL_AE_STATE_CONVERGED
+ )
+ )
+ )
+ delay(FRAME_RATE_MS)
+ }
+ }
+
+ val result = lock3AAsyncTask.await()
+ assertThat(result.isCompleted).isFalse()
+
+ GlobalScope.launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_STATE to CaptureResult
+ .CONTROL_AF_STATE_FOCUSED_LOCKED,
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+ )
+ )
+ )
+ }
+
+ val result3A = result.await()
+ assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.status).isEqualTo(Status3A.OK)
+
+ // There should be one request to monitor AF to finish it's scan.
+ requestProcessor.nextEvent()
+ // One request to lock AE
+ val request2 = requestProcessor.nextEvent().request
+ assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ true
+ )
+
+ // And one request to lock AF.
+ val request3 = requestProcessor.nextEvent().request
+ assertThat(request3!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
+ assertThat(request3.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ true
+ )
+ }
+
+ @Test
+ fun testAfAfterNewScanScanAeAfterNewScan(): Unit = runBlocking {
+ initGraphProcessor()
+
+ val lock3AAsyncTask = GlobalScope.async {
+ controller3A.lock3A(
+ afLockBehavior = Lock3ABehavior.AFTER_NEW_SCAN,
+ aeLockBehavior = Lock3ABehavior.AFTER_NEW_SCAN
+ )
+ }
+ assertThat(lock3AAsyncTask.isCompleted).isFalse()
+ GlobalScope.launch {
+ while (true) {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_STATE to
+ CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
+ CaptureResult.CONTROL_AE_STATE to
+ CaptureResult.CONTROL_AE_STATE_CONVERGED
+ )
+ )
+ )
+ delay(FRAME_RATE_MS)
+ }
+ }
+
+ val result = lock3AAsyncTask.await()
+ assertThat(result.isCompleted).isFalse()
+
+ GlobalScope.launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_STATE to CaptureResult
+ .CONTROL_AF_STATE_FOCUSED_LOCKED,
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+ )
+ )
+ )
+ }
+
+ val result3A = result.await()
+ assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.status).isEqualTo(Status3A.OK)
+
+ // One request to cancel AF to start a new scan.
+ val request1 = requestProcessor.nextEvent().request
+ assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+ )
+ // There should be one request to unlock AE and monitor the current AF scan to finish.
+ val request2 = requestProcessor.nextEvent().request
+ assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ false
+ )
+
+ // There should be one request to monitor lock AE.
+ val request3 = requestProcessor.nextEvent().request
+ assertThat(request3!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ true
+ )
+
+ // And one request to lock AF.
+ val request4 = requestProcessor.nextEvent().request
+ assertThat(request4!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
+ assertThat(request4.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ true
+ )
+ }
+
+ private fun initGraphProcessor() {
+ graphProcessor.attach(requestProcessor)
+ graphProcessor.setRepeating(Request(streams = listOf(StreamId(1))))
+ }
+
+ companion object {
+ // The time duration in milliseconds between two frame results.
+ private const val FRAME_RATE_MS = 33L
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASubmit3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASubmit3ATest.kt
index bf5ea3f..c87f5c0 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASubmit3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASubmit3ATest.kt
@@ -45,9 +45,9 @@
@RunWith(CameraPipeRobolectricTestRunner::class)
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
class Controller3ASubmit3ATest {
- private val requestProcessor = FakeRequestProcessor()
private val graphProcessor = FakeGraphProcessor()
private val graphState3A = GraphState3A()
+ private val requestProcessor = FakeRequestProcessor(graphState3A)
private val listener3A = Listener3A()
private val controller3A = Controller3A(graphProcessor, graphState3A, listener3A)
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUnlock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUnlock3ATest.kt
new file mode 100644
index 0000000..ad3d24c
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUnlock3ATest.kt
@@ -0,0 +1,314 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.impl
+
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.os.Build
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestNumber
+import androidx.camera.camera2.pipe.Status3A
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
+import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
+import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
+import androidx.camera.camera2.pipe.testing.FakeRequestProcessor
+import com.google.common.truth.Truth
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+
+@RunWith(CameraPipeRobolectricTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class Controller3AUnlock3ATest {
+ private val graphProcessor = FakeGraphProcessor()
+ private val graphState3A = GraphState3A()
+ private val requestProcessor = FakeRequestProcessor(graphState3A)
+ private val listener3A = Listener3A()
+ private val controller3A = Controller3A(graphProcessor, graphState3A, listener3A)
+
+ @Test
+ fun testUnlockAe(): Unit = runBlocking {
+ initGraphProcessor()
+
+ val unLock3AAsyncTask = GlobalScope.async {
+ controller3A.unlock3A(ae = true)
+ }
+
+ // Launch a task to repeatedly invoke a given capture result.
+ GlobalScope.launch {
+ while (true) {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AE_STATE to
+ CaptureResult.CONTROL_AE_STATE_LOCKED
+ )
+ )
+ )
+ delay(FRAME_RATE_MS)
+ }
+ }
+
+ val result = unLock3AAsyncTask.await()
+ // Result of unlock3A call shouldn't be complete yet since the AE is locked.
+ Truth.assertThat(result.isCompleted).isFalse()
+
+ // There should be one request to lock AE.
+ val request1 = requestProcessor.nextEvent().request
+ Truth.assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK])
+ .isEqualTo(false)
+
+ GlobalScope.launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_SEARCHING
+ )
+ )
+ )
+ }
+
+ val result3A = result.await()
+ Truth.assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ Truth.assertThat(result3A.status).isEqualTo(Status3A.OK)
+ }
+
+ @Test
+ fun testUnlockAf(): Unit = runBlocking {
+ initGraphProcessor()
+
+ val unLock3AAsyncTask = GlobalScope.async { controller3A.unlock3A(af = true) }
+
+ // Launch a task to repeatedly invoke a given capture result.
+ GlobalScope.launch {
+ while (true) {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_STATE to
+ CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
+ )
+ )
+ )
+ delay(FRAME_RATE_MS)
+ }
+ }
+
+ val result = unLock3AAsyncTask.await()
+ // Result of unlock3A call shouldn't be complete yet since the AF is locked.
+ Truth.assertThat(result.isCompleted).isFalse()
+
+ // There should be one request to unlock AF.
+ val request1 = requestProcessor.nextEvent().request
+ Truth.assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+ .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+
+ GlobalScope.launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_INACTIVE
+ )
+ )
+ )
+ }
+
+ val result3A = result.await()
+ Truth.assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ Truth.assertThat(result3A.status).isEqualTo(Status3A.OK)
+ }
+
+ @Test
+ fun testUnlockAwb(): Unit = runBlocking {
+ initGraphProcessor()
+
+ val unLock3AAsyncTask = GlobalScope.async {
+ controller3A.unlock3A(awb = true)
+ }
+
+ // Launch a task to repeatedly invoke a given capture result.
+ GlobalScope.launch {
+ while (true) {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AWB_STATE to
+ CaptureResult.CONTROL_AWB_STATE_LOCKED
+ )
+ )
+ )
+ delay(FRAME_RATE_MS)
+ }
+ }
+
+ val result = unLock3AAsyncTask.await()
+ // Result of unlock3A call shouldn't be complete yet since the AWB is locked.
+ Truth.assertThat(result.isCompleted).isFalse()
+
+ // There should be one request to lock AWB.
+ val request1 = requestProcessor.nextEvent().request
+ Truth.assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AWB_LOCK])
+ .isEqualTo(false)
+
+ GlobalScope.launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AWB_STATE to CaptureResult.CONTROL_AWB_STATE_SEARCHING
+ )
+ )
+ )
+ }
+
+ val result3A = result.await()
+ Truth.assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ Truth.assertThat(result3A.status).isEqualTo(Status3A.OK)
+ }
+
+ @Test
+ fun testUnlockAeAf(): Unit = runBlocking {
+ initGraphProcessor()
+
+ val unLock3AAsyncTask = GlobalScope.async { controller3A.unlock3A(ae = true, af = true) }
+
+ // Launch a task to repeatedly invoke a given capture result.
+ GlobalScope.launch {
+ while (true) {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED,
+ CaptureResult.CONTROL_AF_STATE to
+ CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
+ )
+ )
+ )
+ delay(FRAME_RATE_MS)
+ }
+ }
+
+ val result = unLock3AAsyncTask.await()
+ // Result of unlock3A call shouldn't be complete yet since the AF is locked.
+ Truth.assertThat(result.isCompleted).isFalse()
+
+ // There should be one request to unlock AF.
+ val request1 = requestProcessor.nextEvent().request
+ Truth.assertThat(request1!!.extraRequestParameters[CaptureRequest.CONTROL_AF_TRIGGER])
+ .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
+ // Then request to unlock AE.
+ val request2 = requestProcessor.nextEvent().request
+ Truth.assertThat(request2!!.extraRequestParameters[CaptureRequest.CONTROL_AE_LOCK])
+ .isEqualTo(false)
+
+ GlobalScope.launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_INACTIVE,
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_SEARCHING
+ )
+ )
+ )
+ }
+
+ val result3A = result.await()
+ Truth.assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ Truth.assertThat(result3A.status).isEqualTo(Status3A.OK)
+ }
+
+ private fun initGraphProcessor() {
+ graphProcessor.attach(requestProcessor)
+ graphProcessor.setRepeating(Request(streams = listOf(StreamId(1))))
+ }
+
+ companion object {
+ // The time duration in milliseconds between two frame results.
+ private const val FRAME_RATE_MS = 33L
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUpdate3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUpdate3ATest.kt
index 833e885..48b00fa 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUpdate3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUpdate3ATest.kt
@@ -24,12 +24,15 @@
import androidx.camera.camera2.pipe.AfMode
import androidx.camera.camera2.pipe.AwbMode
import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.RequestNumber
import androidx.camera.camera2.pipe.Status3A
+import androidx.camera.camera2.pipe.StreamId
import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
+import androidx.camera.camera2.pipe.testing.FakeRequestProcessor
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -45,11 +48,14 @@
class Controller3AUpdate3ATest {
private val graphProcessor = FakeGraphProcessor()
private val graphState3A = GraphState3A()
+ private val requestProcessor = FakeRequestProcessor(graphState3A)
private val listener3A = Listener3A()
private val controller3A = Controller3A(graphProcessor, graphState3A, listener3A)
@Test
fun testUpdate3AUpdatesState3A() {
+ initGraphProcessor()
+
val result = controller3A.update3A(afMode = AfMode.OFF)
assertThat(graphState3A.readState()[CaptureRequest.CONTROL_AF_MODE]).isEqualTo(
CaptureRequest.CONTROL_AE_MODE_OFF
@@ -60,6 +66,8 @@
@ExperimentalCoroutinesApi
@Test
fun testUpdate3ACancelsPreviousInProgressUpdate() {
+ initGraphProcessor()
+
val result = controller3A.update3A(afMode = AfMode.OFF)
// Invoking update3A before the previous one is complete will cancel the result of the
// previous call.
@@ -69,6 +77,8 @@
@Test
fun testAfModeUpdate(): Unit = runBlocking {
+ initGraphProcessor()
+
val result = controller3A.update3A(afMode = AfMode.OFF)
GlobalScope.launch {
listener3A.onRequestSequenceCreated(
@@ -94,6 +104,8 @@
@Test
fun testAeModeUpdate(): Unit = runBlocking {
+ initGraphProcessor()
+
val result = controller3A.update3A(aeMode = AeMode.ON_ALWAYS_FLASH)
GlobalScope.launch {
listener3A.onRequestSequenceCreated(
@@ -120,6 +132,8 @@
@Test
fun testAwbModeUpdate(): Unit = runBlocking {
+ initGraphProcessor()
+
val result = controller3A.update3A(awbMode = AwbMode.CLOUDY_DAYLIGHT)
GlobalScope.launch {
listener3A.onRequestSequenceCreated(
@@ -146,6 +160,8 @@
@Test
fun testAfRegionsUpdate(): Unit = runBlocking {
+ initGraphProcessor()
+
val result = controller3A.update3A(afRegions = listOf(MeteringRectangle(1, 1, 100, 100, 2)))
GlobalScope.launch {
listener3A.onRequestSequenceCreated(
@@ -172,6 +188,8 @@
@Test
fun testAeRegionsUpdate(): Unit = runBlocking {
+ initGraphProcessor()
+
val result = controller3A.update3A(aeRegions = listOf(MeteringRectangle(1, 1, 100, 100, 2)))
GlobalScope.launch {
listener3A.onRequestSequenceCreated(
@@ -198,6 +216,8 @@
@Test
fun testAwbRegionsUpdate(): Unit = runBlocking {
+ initGraphProcessor()
+
val result = controller3A.update3A(
awbRegions = listOf(
MeteringRectangle(1, 1, 100, 100, 2)
@@ -225,4 +245,9 @@
assertThat(result3A.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Status3A.OK)
}
+
+ private fun initGraphProcessor() {
+ graphProcessor.attach(requestProcessor)
+ graphProcessor.setRepeating(Request(streams = listOf(StreamId(1))))
+ }
}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt
index abdc307..23c63d1 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt
@@ -36,8 +36,8 @@
class GraphProcessorTest {
private val globalListener = FakeRequestListener()
- private val fakeProcessor1 = FakeRequestProcessor()
- private val fakeProcessor2 = FakeRequestProcessor()
+ private val fakeProcessor1 = FakeRequestProcessor(GraphState3A())
+ private val fakeProcessor2 = FakeRequestProcessor(GraphState3A())
private val requestListener1 = FakeRequestListener()
private val request1 = Request(listOf(StreamId(0)), listeners = listOf(requestListener1))
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/SessionFactoryTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/SessionFactoryTest.kt
index 730a1dd..f8007e9 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/SessionFactoryTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/SessionFactoryTest.kt
@@ -105,7 +105,7 @@
virtualSessionState = VirtualSessionState(
FakeGraphProcessor(),
sessionFactory,
- FakeRequestProcessor(),
+ FakeRequestProcessor(GraphState3A()),
this
)
)
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
index 139f0b0..5ab64bc 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
@@ -93,5 +93,6 @@
}
override fun invalidate() {
+ processor!!.setRepeating(repeatingRequest!!, mapOf(), false)
}
}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt
index f7e3bed..0f446d5 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt
@@ -20,6 +20,7 @@
import android.view.Surface
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.impl.GraphState3A
import androidx.camera.camera2.pipe.impl.RequestProcessor
import androidx.camera.camera2.pipe.impl.TokenLock
import androidx.camera.camera2.pipe.impl.TokenLockImpl
@@ -30,7 +31,8 @@
/**
* Fake implementation of a [RequestProcessor] for tests.
*/
-class FakeRequestProcessor : RequestProcessor, RequestProcessor.Factory {
+class FakeRequestProcessor(private val graphState3A: GraphState3A) :
+ RequestProcessor, RequestProcessor.Factory {
private val eventChannel = Channel<Event>(Channel.UNLIMITED)
val requestQueue: MutableList<FakeRequest> = mutableListOf()
@@ -71,7 +73,7 @@
requireSurfacesForAllStreams: Boolean
): Boolean {
val fakeRequest =
- FakeRequest(listOf(request), extraRequestParameters, requireSurfacesForAllStreams)
+ createFakeRequest(listOf(request), extraRequestParameters, requireSurfacesForAllStreams)
if (rejectRequests || closeInvoked) {
check(eventChannel.offer(Event(request = fakeRequest, rejected = true)))
@@ -90,7 +92,7 @@
requireSurfacesForAllStreams: Boolean
): Boolean {
val fakeRequest =
- FakeRequest(requests, extraRequestParameters, requireSurfacesForAllStreams)
+ createFakeRequest(requests, extraRequestParameters, requireSurfacesForAllStreams)
if (rejectRequests || closeInvoked) {
check(eventChannel.offer(Event(request = fakeRequest, rejected = true)))
return false
@@ -108,7 +110,7 @@
requireSurfacesForAllStreams: Boolean
): Boolean {
val fakeRequest =
- FakeRequest(listOf(request), extraRequestParameters, requireSurfacesForAllStreams)
+ createFakeRequest(listOf(request), extraRequestParameters, requireSurfacesForAllStreams)
if (rejectRequests || closeInvoked) {
check(eventChannel.offer(Event(request = fakeRequest, rejected = true)))
return false
@@ -137,9 +139,20 @@
/**
* Get the next event from queue with an option to specify a timeout for tests.
*/
- suspend fun nextEvent(timeMillis: Long = 100): Event = withTimeout(timeMillis) {
+ suspend fun nextEvent(timeMillis: Long = 500): Event = withTimeout(timeMillis) {
eventChannel.receive()
}
+
+ private fun createFakeRequest(
+ burst: List<Request>,
+ extraRequestParameters: Map<CaptureRequest.Key<*>, Any>,
+ requireStreams: Boolean
+ ): FakeRequest {
+ val parameterMap = mutableMapOf<CaptureRequest.Key<*>, Any>()
+ parameterMap.putAll(graphState3A.readState())
+ parameterMap.putAll(extraRequestParameters)
+ return FakeRequest(burst, parameterMap, requireStreams)
+ }
}
data class Event(
diff --git a/camera/camera-camera2/build.gradle b/camera/camera-camera2/build.gradle
index 33822d5..02e92f9 100644
--- a/camera/camera-camera2/build.gradle
+++ b/camera/camera-camera2/build.gradle
@@ -63,7 +63,8 @@
androidTestImplementation(KOTLIN_COROUTINES_ANDROID)
androidTestImplementation(project(":annotation:annotation-experimental"))
androidTestImplementation(project(":internal-testutils-truth"))
- androidTestImplementation("org.jetbrains.kotlinx:atomicfu:0.13.1")
+ androidTestImplementation("org.jetbrains.kotlinx:atomicfu:0.15.0")
+ androidTestImplementation("androidx.exifinterface:exifinterface:1.0.0")
}
android {
defaultConfig {
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraCaptureResultTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraCaptureResultTest.java
index ef5af22..5001b38 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraCaptureResultTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraCaptureResultTest.java
@@ -16,10 +16,14 @@
package androidx.camera.camera2.internal;
+import static androidx.exifinterface.media.ExifInterface.FLAG_FLASH_FIRED;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.when;
+import android.graphics.Rect;
+import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.CaptureResult;
import androidx.camera.core.impl.CameraCaptureMetaData.AeState;
@@ -28,6 +32,8 @@
import androidx.camera.core.impl.CameraCaptureMetaData.AwbState;
import androidx.camera.core.impl.CameraCaptureMetaData.FlashState;
import androidx.camera.core.impl.TagBundle;
+import androidx.camera.core.impl.utils.ExifData;
+import androidx.exifinterface.media.ExifInterface;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -36,13 +42,15 @@
import org.junit.runner.RunWith;
import org.mockito.Mockito;
+import java.util.concurrent.TimeUnit;
+
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class Camera2CameraCaptureResultTest {
private CaptureResult mCaptureResult;
private Camera2CameraCaptureResult mCamera2CameraCaptureResult;
- private TagBundle mTag = TagBundle.emptyBundle();
+ private final TagBundle mTag = TagBundle.emptyBundle();
@Before
public void setUp() {
@@ -275,4 +283,74 @@
.thenReturn(CaptureResult.FLASH_STATE_PARTIAL);
assertThat(mCamera2CameraCaptureResult.getFlashState()).isEqualTo(FlashState.FIRED);
}
+
+ @Test
+ public void canPopulateExif() {
+ // Arrange
+ when(mCaptureResult.get(CaptureResult.FLASH_STATE))
+ .thenReturn(CaptureResult.FLASH_STATE_FIRED);
+
+ Rect cropRegion = new Rect(0, 0, 640, 480);
+ when(mCaptureResult.get(CaptureResult.SCALER_CROP_REGION)).thenReturn(cropRegion);
+
+ when(mCaptureResult.get(CaptureResult.JPEG_ORIENTATION)).thenReturn(270);
+
+ long exposureTime = TimeUnit.SECONDS.toNanos(5);
+ when(mCaptureResult.get(CaptureResult.SENSOR_EXPOSURE_TIME)).thenReturn(exposureTime);
+
+ float aperture = 1.8f;
+ when(mCaptureResult.get(CaptureResult.LENS_APERTURE)).thenReturn(aperture);
+
+ int iso = 200;
+ int postRawSensitivityBoost = 200;
+ when(mCaptureResult.get(CaptureResult.SENSOR_SENSITIVITY)).thenReturn(iso);
+ when(mCaptureResult.get(CaptureResult.CONTROL_POST_RAW_SENSITIVITY_BOOST))
+ .thenReturn(postRawSensitivityBoost);
+
+ float focalLength = 4200f;
+ when(mCaptureResult.get(CaptureResult.LENS_FOCAL_LENGTH)).thenReturn(focalLength);
+
+ when(mCaptureResult.get(CaptureResult.CONTROL_AWB_MODE))
+ .thenReturn(CameraMetadata.CONTROL_AWB_MODE_OFF);
+
+ // Act
+ ExifData.Builder exifBuilder = ExifData.builderForDevice();
+ mCamera2CameraCaptureResult.populateExifData(exifBuilder);
+ ExifData exifData = exifBuilder.build();
+
+ // Assert
+ assertThat(Short.parseShort(exifData.getAttribute(ExifInterface.TAG_FLASH)))
+ .isEqualTo(FLAG_FLASH_FIRED);
+
+ assertThat(exifData.getAttribute(ExifInterface.TAG_IMAGE_WIDTH))
+ .isEqualTo(String.valueOf(cropRegion.width()));
+
+ assertThat(exifData.getAttribute(ExifInterface.TAG_IMAGE_LENGTH))
+ .isEqualTo(String.valueOf(cropRegion.height()));
+
+ assertThat(exifData.getAttribute(ExifInterface.TAG_ORIENTATION))
+ .isEqualTo(String.valueOf(ExifInterface.ORIENTATION_ROTATE_270));
+
+ String exposureTimeString = exifData.getAttribute(ExifInterface.TAG_EXPOSURE_TIME);
+ assertThat(exposureTimeString).isNotNull();
+ assertThat(Float.parseFloat(exposureTimeString)).isWithin(0.1f)
+ .of(TimeUnit.NANOSECONDS.toSeconds(exposureTime));
+
+ assertThat(exifData.getAttribute(ExifInterface.TAG_F_NUMBER))
+ .isEqualTo(String.valueOf(aperture));
+
+ assertThat(
+ Short.parseShort(exifData.getAttribute(ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY)))
+ .isEqualTo((short) (iso * (int) (postRawSensitivityBoost / 100f)));
+
+ String focalLengthString = exifData.getAttribute(ExifInterface.TAG_FOCAL_LENGTH);
+ assertThat(focalLengthString).isNotNull();
+ String[] fractionValues = focalLengthString.split("/");
+ long numerator = Long.parseLong(fractionValues[0]);
+ long denominator = Long.parseLong(fractionValues[1]);
+ assertThat(numerator / (float) denominator).isWithin(0.1f).of(focalLength);
+
+ assertThat(Short.parseShort(exifData.getAttribute(ExifInterface.TAG_WHITE_BALANCE)))
+ .isEqualTo(ExifInterface.WHITE_BALANCE_MANUAL);
+ }
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraCaptureResult.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraCaptureResult.java
index c4a51cc..928d8ef 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraCaptureResult.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraCaptureResult.java
@@ -16,7 +16,10 @@
package androidx.camera.camera2.internal;
+import android.graphics.Rect;
+import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.CaptureResult;
+import android.os.Build;
import androidx.annotation.NonNull;
import androidx.camera.core.Logger;
@@ -27,6 +30,7 @@
import androidx.camera.core.impl.CameraCaptureMetaData.FlashState;
import androidx.camera.core.impl.CameraCaptureResult;
import androidx.camera.core.impl.TagBundle;
+import androidx.camera.core.impl.utils.ExifData;
/** The camera2 implementation for the capture result of a single image capture. */
public class Camera2CameraCaptureResult implements CameraCaptureResult {
@@ -203,6 +207,66 @@
return mTagBundle;
}
+ @Override
+ public void populateExifData(@NonNull ExifData.Builder exifData) {
+ // Call interface default to set flash mode
+ CameraCaptureResult.super.populateExifData(exifData);
+
+ // Set dimensions
+ Rect cropRegion = mCaptureResult.get(CaptureResult.SCALER_CROP_REGION);
+ if (cropRegion != null) {
+ exifData.setImageWidth(cropRegion.width())
+ .setImageHeight(cropRegion.height());
+ }
+
+ // Set orientation
+ Integer jpegOrientation = mCaptureResult.get(CaptureResult.JPEG_ORIENTATION);
+ if (jpegOrientation != null) {
+ exifData.setOrientationDegrees(jpegOrientation);
+ }
+
+ // Set exposure time
+ Long exposureTimeNs = mCaptureResult.get(CaptureResult.SENSOR_EXPOSURE_TIME);
+ if (exposureTimeNs != null) {
+ exifData.setExposureTimeNanos(exposureTimeNs);
+ }
+
+ // Set the aperture
+ Float aperture = mCaptureResult.get(CaptureResult.LENS_APERTURE);
+ if (aperture != null) {
+ exifData.setLensFNumber(aperture);
+ }
+
+ // Set the ISO
+ Integer iso = mCaptureResult.get(CaptureResult.SENSOR_SENSITIVITY);
+ if (iso != null) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ Integer postRawSensitivityBoost =
+ mCaptureResult.get(CaptureResult.CONTROL_POST_RAW_SENSITIVITY_BOOST);
+ if (postRawSensitivityBoost != null) {
+ iso *= (int) (postRawSensitivityBoost / 100f);
+ }
+ }
+ exifData.setIso(iso);
+ }
+
+ // Set the focal length
+ Float focalLength = mCaptureResult.get(CaptureResult.LENS_FOCAL_LENGTH);
+ if (focalLength != null) {
+ exifData.setFocalLength(focalLength);
+ }
+
+ // Set white balance MANUAL/AUTO
+ Integer whiteBalanceMode = mCaptureResult.get(CaptureResult.CONTROL_AWB_MODE);
+ if (whiteBalanceMode != null) {
+ ExifData.WhiteBalanceMode wbMode = ExifData.WhiteBalanceMode.AUTO;
+ if (whiteBalanceMode == CameraMetadata.CONTROL_AWB_MODE_OFF) {
+ wbMode = ExifData.WhiteBalanceMode.MANUAL;
+ }
+ exifData.setWhiteBalanceMode(wbMode);
+ }
+ }
+
@NonNull
public CaptureResult getCaptureResult() {
return mCaptureResult;
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
index 7adafc2..c3df3e65 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
@@ -37,7 +37,6 @@
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.core.CameraUnavailableException;
import androidx.camera.core.Logger;
import androidx.camera.core.Preview;
@@ -51,7 +50,6 @@
import androidx.camera.core.impl.ImmediateSurface;
import androidx.camera.core.impl.LiveDataObservable;
import androidx.camera.core.impl.Observable;
-import androidx.camera.core.impl.Quirks;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.SessionConfig.ValidatingBuilder;
import androidx.camera.core.impl.UseCaseAttachState;
@@ -168,9 +166,6 @@
private final SynchronizedCaptureSessionOpener.Builder mCaptureSessionOpenerBuilder;
private final Set<String> mNotifyStateAttachedSet = new HashSet<>();
- @NonNull
- private final Quirks mCameraQuirks;
-
/**
* Constructor for a camera.
*
@@ -203,10 +198,9 @@
try {
CameraCharacteristicsCompat cameraCharacteristicsCompat =
mCameraManager.getCameraCharacteristicsCompat(cameraId);
- mCameraQuirks = CameraQuirks.get(cameraId, cameraCharacteristicsCompat);
mCameraControlInternal = new Camera2CameraControlImpl(cameraCharacteristicsCompat,
executorScheduler, mExecutor, new ControlUpdateListenerInternal(),
- mCameraQuirks);
+ cameraInfoImpl.getCameraQuirks());
mCameraInfoInternal = cameraInfoImpl;
mCameraInfoInternal.linkWithCameraControl(mCameraControlInternal);
} catch (CameraAccessExceptionCompat e) {
@@ -880,13 +874,6 @@
return mCameraInfoInternal;
}
- /** {@inheritDoc} */
- @NonNull
- @Override
- public Quirks getCameraQuirks() {
- return mCameraQuirks;
- }
-
/** Opens the camera device */
// TODO(b/124268878): Handle SecurityException and require permission in manifest.
@SuppressLint("MissingPermission")
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 43c735b..b31c41c 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
@@ -26,6 +26,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.experimental.UseExperimental;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.camera2.internal.compat.quirk.CameraQuirks;
import androidx.camera.camera2.interop.Camera2CameraInfo;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.CameraSelector;
@@ -36,6 +37,7 @@
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
+import androidx.camera.core.impl.Quirks;
import androidx.camera.core.impl.utils.CameraOrientationUtil;
import androidx.core.util.Preconditions;
import androidx.lifecycle.LiveData;
@@ -80,6 +82,9 @@
@Nullable
private List<Pair<CameraCaptureCallback, Executor>> mCameraCaptureCallbacks = null;
+ @NonNull
+ private final Quirks mCameraQuirks;
+
/**
* Constructs an instance. Before {@link #linkWithCameraControl(Camera2CameraControlImpl)} is
* called, camera control related API (torch/exposure/zoom) will return default values.
@@ -89,6 +94,7 @@
mCameraId = Preconditions.checkNotNull(cameraId);
mCameraCharacteristicsCompat = cameraCharacteristicsCompat;
mCamera2CameraInfo = new Camera2CameraInfo(this);
+ mCameraQuirks = CameraQuirks.get(cameraId, cameraCharacteristicsCompat);
}
/**
@@ -336,6 +342,13 @@
}
}
+ /** {@inheritDoc} */
+ @NonNull
+ @Override
+ public Quirks getCameraQuirks() {
+ return mCameraQuirks;
+ }
+
/**
* Gets the implementation of {@link Camera2CameraInfo}.
*/
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 0ec8db2..85d73124c 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
@@ -372,7 +372,9 @@
useCases.add(preview);
Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
- Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+ Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+ mCameraFactory.getCamera(LEGACY_CAMERA_ID).getCameraInfoInternal(),
+ useCases,
mUseCaseConfigFactory);
// A legacy level camera device can't support JPEG (ImageCapture) + PRIV (VideoCapture) +
// PRIV (Preview) combination. An IllegalArgumentException will be thrown when trying to
@@ -399,7 +401,9 @@
useCases.add(preview);
Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
- Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+ Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+ mCameraFactory.getCamera(LIMITED_CAMERA_ID).getCameraInfoInternal(),
+ useCases,
mUseCaseConfigFactory);
Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
mSurfaceManager.getSuggestedResolutions(LIMITED_CAMERA_ID, Collections.emptyList(),
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 368e5fe..5448c5d 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
@@ -495,7 +495,9 @@
List<UseCase> useCases = new ArrayList<>();
useCases.add(fakeUseCase);
Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
- Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+ Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+ mCameraFactory.getCamera(CAMERA_ID).getCameraInfoInternal(),
+ useCases,
mUseCaseConfigFactory);
Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
supportedSurfaceCombination.getSuggestedResolutions(Collections.emptyList(),
@@ -605,7 +607,9 @@
useCases.add(imageCapture);
useCases.add(imageAnalysis);
Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
- Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+ Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+ mCameraFactory.getCamera(CAMERA_ID).getCameraInfoInternal(),
+ useCases,
mUseCaseConfigFactory);
Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
supportedSurfaceCombination.getSuggestedResolutions(Collections.emptyList(),
@@ -653,7 +657,9 @@
List<UseCase> useCases = new ArrayList<>();
useCases.add(preview);
Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
- Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+ Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+ mCameraFactory.getCamera(CAMERA_ID).getCameraInfoInternal(),
+ useCases,
mUseCaseConfigFactory);
Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
supportedSurfaceCombination.getSuggestedResolutions(Collections.emptyList(),
@@ -743,7 +749,9 @@
useCases.add(videoCapture);
useCases.add(preview);
Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
- Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+ Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+ mCameraFactory.getCamera(CAMERA_ID).getCameraInfoInternal(),
+ useCases,
mUseCaseConfigFactory);
Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
supportedSurfaceCombination.getSuggestedResolutions(Collections.emptyList(),
@@ -775,7 +783,9 @@
useCases.add(preview);
Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
- Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+ Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+ mCameraFactory.getCamera(CAMERA_ID).getCameraInfoInternal(),
+ useCases,
mUseCaseConfigFactory);
Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
supportedSurfaceCombination.getSuggestedResolutions(Collections.emptyList(),
@@ -818,7 +828,9 @@
useCases.add(preview);
useCases.add(imageAnalysis);
Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
- Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+ Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+ mCameraFactory.getCamera(CAMERA_ID).getCameraInfoInternal(),
+ useCases,
mUseCaseConfigFactory);
Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
supportedSurfaceCombination.getSuggestedResolutions(Collections.emptyList(),
@@ -853,7 +865,9 @@
useCases.add(preview);
useCases.add(imageAnalysis);
Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
- Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+ Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+ mCameraFactory.getCamera(CAMERA_ID).getCameraInfoInternal(),
+ useCases,
mUseCaseConfigFactory);
Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
supportedSurfaceCombination.getSuggestedResolutions(Collections.emptyList(),
@@ -936,7 +950,9 @@
useCases.add(videoCapture);
useCases.add(preview);
Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
- Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+ Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+ mCameraFactory.getCamera(CAMERA_ID).getCameraInfoInternal(),
+ useCases,
mUseCaseConfigFactory);
Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
supportedSurfaceCombination.getSuggestedResolutions(Collections.emptyList(),
@@ -1117,7 +1133,9 @@
useCases.add(imageCapture);
Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap =
- Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(useCases,
+ Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+ mCameraFactory.getCamera(CAMERA_ID).getCameraInfoInternal(),
+ useCases,
mUseCaseConfigFactory);
Map<UseCaseConfig<?>, Size> suggestedResolutionMap =
supportedSurfaceCombination.getSuggestedResolutions(Collections.emptyList(),
diff --git a/camera/camera-core/api/public_plus_experimental_1.0.0-beta12.txt b/camera/camera-core/api/public_plus_experimental_1.0.0-beta12.txt
index e432572..45f7159b 100644
--- a/camera/camera-core/api/public_plus_experimental_1.0.0-beta12.txt
+++ b/camera/camera-core/api/public_plus_experimental_1.0.0-beta12.txt
@@ -69,12 +69,14 @@
}
public final class CameraXConfig {
+ method @androidx.camera.core.ExperimentalAvailableCamerasLimiter public androidx.camera.core.CameraSelector? getAvailableCamerasSelector(androidx.camera.core.CameraSelector?);
method @androidx.camera.core.ExperimentalLogging public int getMinimumLoggingLevel();
}
public static final class CameraXConfig.Builder {
method public androidx.camera.core.CameraXConfig build();
method public static androidx.camera.core.CameraXConfig.Builder fromConfig(androidx.camera.core.CameraXConfig);
+ method @androidx.camera.core.ExperimentalAvailableCamerasLimiter public androidx.camera.core.CameraXConfig.Builder setAvailableCamerasSelector(androidx.camera.core.CameraSelector);
method public androidx.camera.core.CameraXConfig.Builder setCameraExecutor(java.util.concurrent.Executor);
method @androidx.camera.core.ExperimentalLogging public androidx.camera.core.CameraXConfig.Builder setMinimumLoggingLevel(@IntRange(from=android.util.Log.DEBUG, to=android.util.Log.ERROR) int);
method @androidx.camera.core.ExperimentalCustomizableThreads public androidx.camera.core.CameraXConfig.Builder setSchedulerHandler(android.os.Handler);
@@ -88,6 +90,9 @@
ctor public DisplayOrientedMeteringPointFactory(android.view.Display, androidx.camera.core.CameraInfo, float, float);
}
+ @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAvailableCamerasSelector {
+ }
+
@experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCameraFilter {
}
diff --git a/camera/camera-core/api/public_plus_experimental_current.txt b/camera/camera-core/api/public_plus_experimental_current.txt
index e432572..b10368c 100644
--- a/camera/camera-core/api/public_plus_experimental_current.txt
+++ b/camera/camera-core/api/public_plus_experimental_current.txt
@@ -69,12 +69,14 @@
}
public final class CameraXConfig {
+ method @androidx.camera.core.ExperimentalAvailableCamerasLimiter public androidx.camera.core.CameraSelector? getAvailableCamerasLimiter(androidx.camera.core.CameraSelector?);
method @androidx.camera.core.ExperimentalLogging public int getMinimumLoggingLevel();
}
public static final class CameraXConfig.Builder {
method public androidx.camera.core.CameraXConfig build();
method public static androidx.camera.core.CameraXConfig.Builder fromConfig(androidx.camera.core.CameraXConfig);
+ method @androidx.camera.core.ExperimentalAvailableCamerasLimiter public androidx.camera.core.CameraXConfig.Builder setAvailableCamerasLimiter(androidx.camera.core.CameraSelector);
method public androidx.camera.core.CameraXConfig.Builder setCameraExecutor(java.util.concurrent.Executor);
method @androidx.camera.core.ExperimentalLogging public androidx.camera.core.CameraXConfig.Builder setMinimumLoggingLevel(@IntRange(from=android.util.Log.DEBUG, to=android.util.Log.ERROR) int);
method @androidx.camera.core.ExperimentalCustomizableThreads public androidx.camera.core.CameraXConfig.Builder setSchedulerHandler(android.os.Handler);
@@ -88,6 +90,9 @@
ctor public DisplayOrientedMeteringPointFactory(android.view.Display, androidx.camera.core.CameraInfo, float, float);
}
+ @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAvailableCamerasLimiter {
+ }
+
@experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCameraFilter {
}
diff --git a/camera/camera-core/build.gradle b/camera/camera-core/build.gradle
index 0615e08..49d045a 100644
--- a/camera/camera-core/build.gradle
+++ b/camera/camera-core/build.gradle
@@ -15,7 +15,6 @@
*/
import static androidx.build.dependencies.DependenciesKt.*
-import androidx.build.LibraryVersions
import androidx.build.LibraryGroups
import androidx.build.Publish
@@ -48,6 +47,7 @@
testImplementation project(":camera:camera-testing"), {
exclude group: "androidx.camera", module: "camera-core"
}
+ testImplementation("androidx.exifinterface:exifinterface:1.0.0")
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
androidTestImplementation(ANDROIDX_TEST_CORE)
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.java
index 34cba04..d6adbe0 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.java
@@ -37,6 +37,7 @@
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.UseCaseConfig;
+import androidx.camera.testing.fakes.FakeCameraInfoInternal;
import androidx.camera.testing.fakes.FakeUseCase;
import androidx.camera.testing.fakes.FakeUseCaseConfig;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -188,7 +189,10 @@
TestUseCase testUseCase = new TestUseCase(useCaseConfig);
- UseCaseConfig<?> mergedConfig = testUseCase.mergeConfigs(extendedConfig, defaultConfig);
+ FakeCameraInfoInternal cameraInfo = new FakeCameraInfoInternal();
+
+ UseCaseConfig<?> mergedConfig = testUseCase.mergeConfigs(cameraInfo, extendedConfig,
+ defaultConfig);
assertThat(mergedConfig.getSurfaceOccupancyPriority()).isEqualTo(cameraDefaultPriority);
assertThat(mergedConfig.getInputFormat()).isEqualTo(useCaseImageFormat);
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/utils/ExifOutputStreamTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/utils/ExifOutputStreamTest.kt
new file mode 100644
index 0000000..8b88aa5
--- /dev/null
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/utils/ExifOutputStreamTest.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.
+ */
+
+package androidx.camera.core.impl.utils
+
+import android.graphics.Bitmap
+import android.os.Build
+import androidx.camera.core.impl.CameraCaptureMetaData
+import androidx.exifinterface.media.ExifInterface
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import java.io.File
+
+@LargeTest
+public class ExifOutputStreamTest {
+
+ @Test
+ public fun canSetExifOnCompressedBitmap() {
+ // Arrange.
+ val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
+ val exifData = ExifData.builderForDevice()
+ .setImageWidth(bitmap.width)
+ .setImageHeight(bitmap.height)
+ .setFlashState(CameraCaptureMetaData.FlashState.NONE)
+ .setExposureTimeNanos(0)
+ .build()
+
+ val fileWithExif = File.createTempFile("testWithExif", ".jpg")
+ val outputStreamWithExif = ExifOutputStream(fileWithExif.outputStream(), exifData)
+ fileWithExif.deleteOnExit()
+ val fileWithoutExif = File.createTempFile("testWithoutExif", ".jpg")
+ val outputStreamWithoutExif = fileWithoutExif.outputStream()
+ fileWithoutExif.deleteOnExit()
+
+ // Act.
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 95, outputStreamWithExif)
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 95, outputStreamWithoutExif)
+
+ // Verify with ExifInterface
+ val withExif = ExifInterface(fileWithExif.inputStream())
+ val withoutExif = ExifInterface(fileWithoutExif.inputStream())
+
+ // Assert.
+ // Model comes from default builder
+ assertThat(withExif.getAttribute(ExifInterface.TAG_MODEL)).isEqualTo(Build.MODEL)
+ assertThat(withoutExif.getAttribute(ExifInterface.TAG_MODEL)).isNull()
+
+ assertThat(withExif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH)).isEqualTo("100")
+ assertThat(withoutExif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH)).isEqualTo("100")
+
+ assertThat(withExif.getAttribute(ExifInterface.TAG_IMAGE_LENGTH)).isEqualTo("100")
+ assertThat(withoutExif.getAttribute(ExifInterface.TAG_IMAGE_LENGTH)).isEqualTo("100")
+
+ assertThat(withExif.getAttribute(ExifInterface.TAG_FLASH)?.toShort())
+ .isEqualTo(ExifInterface.FLAG_FLASH_NO_FLASH_FUNCTION)
+ assertThat(withoutExif.getAttribute(ExifInterface.TAG_FLASH)).isNull()
+
+ assertThat(withExif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME)?.toFloat()?.toInt())
+ .isEqualTo(0)
+ assertThat(withoutExif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME)).isNull()
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
index 13e5d31..e4d5954 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
@@ -32,7 +32,8 @@
import java.util.List;
/**
- * A set of requirements and priorities used to select a camera.
+ * A set of requirements and priorities used to select a camera or return a filtered set of
+ * cameras.
*/
public final class CameraSelector {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
index ea5eea6..2d630a2 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
@@ -31,6 +31,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.experimental.UseExperimental;
import androidx.camera.core.impl.CameraDeviceSurfaceManager;
import androidx.camera.core.impl.CameraFactory;
import androidx.camera.core.impl.CameraInternal;
@@ -538,6 +539,7 @@
/**
* Initializes camera stack on the given thread and retry recursively until timeout.
*/
+ @UseExperimental(markerClass = ExperimentalAvailableCamerasLimiter.class)
private void initAndRetryRecursively(
@NonNull Executor cameraExecutor,
long startMs,
@@ -562,8 +564,10 @@
CameraThreadConfig cameraThreadConfig = CameraThreadConfig.create(mCameraExecutor,
mSchedulerHandler);
+ CameraSelector availableCamerasLimiter =
+ mCameraXConfig.getAvailableCamerasLimiter(null);
mCameraFactory = cameraFactoryProvider.newInstance(mAppContext,
- cameraThreadConfig, null);
+ cameraThreadConfig, availableCamerasLimiter);
CameraDeviceSurfaceManager.Provider surfaceManagerProvider =
mCameraXConfig.getDeviceSurfaceManagerProvider(null);
if (surfaceManagerProvider == null) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraXConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraXConfig.java
index 7290fb4..4ac5e41 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraXConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraXConfig.java
@@ -101,6 +101,10 @@
Option.create(
"camerax.core.appConfig.minimumLoggingLevel",
int.class);
+ static final Option<CameraSelector> OPTION_AVAILABLE_CAMERAS_LIMITER =
+ Option.create(
+ "camerax.core.appConfig.availableCamerasLimiter",
+ CameraSelector.class);
// *********************************************************************************************
@@ -178,6 +182,16 @@
return mConfig.retrieveOption(OPTION_MIN_LOGGING_LEVEL, Logger.DEFAULT_MIN_LOG_LEVEL);
}
+ /**
+ * Returns the {@link CameraSelector} used to determine the available cameras.
+ */
+ @ExperimentalAvailableCamerasLimiter
+ @Nullable
+ public CameraSelector getAvailableCamerasLimiter(@Nullable CameraSelector valueIfMissing) {
+ return mConfig.retrieveOption(OPTION_AVAILABLE_CAMERAS_LIMITER, valueIfMissing);
+ }
+
+
/** @hide */
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
@@ -329,6 +343,32 @@
return this;
}
+ /**
+ * Sets a {@link CameraSelector} to determine the available cameras which defines
+ * which cameras can be used in the application.
+ *
+ * <p>Only cameras selected by this CameraSelector can be used in the applications. If
+ * the application binds the use cases with a CameraSelector that selects a unavailable
+ * camera, a {@link IllegalArgumentException} will be thrown.
+ *
+ * <p>This configuration can help CameraX optimize the latency of CameraX initialization.
+ * The tasks CameraX initialization performs include enumerating cameras, querying
+ * CameraCharacteristics and retrieving properties preparing for resolution determination.
+ * On some low end devices, these could take significant amount of time. Using the API
+ * can avoid the initialization of unnecessary cameras and speed up the time for camera
+ * start-up. For example, if the application uses only back cameras, it can set this
+ * configuration by CameraSelector.DEFAULT_BACK_CAMERA and then CameraX will avoid
+ * initializing front cameras to reduce the latency.
+ */
+ @ExperimentalAvailableCamerasLimiter
+ @NonNull
+ public Builder setAvailableCamerasLimiter(
+ @NonNull CameraSelector availableCameraSelector) {
+ getMutableConfig().insertOption(OPTION_AVAILABLE_CAMERAS_LIMITER,
+ availableCameraSelector);
+ return this;
+ }
+
@NonNull
private MutableConfig getMutableConfig() {
return mMutableConfig;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalAvailableCamerasLimiter.java b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalAvailableCamerasLimiter.java
new file mode 100644
index 0000000..99fc74f
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalAvailableCamerasLimiter.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import androidx.annotation.experimental.Experimental;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Denotes that the annotated method uses an experimental API that configures CameraX to
+ * limit the available cameras applications can use in order to optimize the initialization
+ * latency.
+ *
+ * <p>Once the configuration is enabled, only cameras selected by this CameraSelector can be used
+ * in the applications. If the application binds the use cases with a CameraSelector that selects
+ * a unavailable camera, a {@link IllegalArgumentException} will be thrown.
+ *
+ * <p>CameraX initialization performs tasks including enumerating cameras, querying
+ * CameraCharacteristics and retrieving properties preparing for resolution determination. On
+ * some low end devices, these could take significant amount of time. Using the API can avoid the
+ * initialization of unnecessary cameras and speed up the time for camera start-up. For example,
+ * if the application uses only back cameras, it can set this configuration by
+ * CameraSelector.DEFAULT_BACK_CAMERA and then CameraX will avoid initializing front cameras to
+ * reduce the latency.
+ */
+@Retention(CLASS)
+@Experimental
+public @interface ExperimentalAvailableCamerasLimiter {
+}
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 a962f19..16e340b 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
@@ -80,6 +80,7 @@
import androidx.camera.core.impl.CameraCaptureMetaData.AwbState;
import androidx.camera.core.impl.CameraCaptureResult;
import androidx.camera.core.impl.CameraCaptureResult.EmptyCameraCaptureResult;
+import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.CaptureBundle;
import androidx.camera.core.impl.CaptureConfig;
@@ -463,7 +464,8 @@
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
@Override
- UseCaseConfig<?> onMergeConfig(@NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
+ UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
+ @NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
// Update the input format base on the other options set (mainly whether processing
// is done)
Integer bufferFormat = builder.getMutableConfig().retrieveOption(OPTION_BUFFER_FORMAT,
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageInfo.java
index b8a2b6b..f5feed0 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageInfo.java
@@ -19,6 +19,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.camera.core.impl.TagBundle;
+import androidx.camera.core.impl.utils.ExifData;
/** Metadata for an image. */
public interface ImageInfo {
@@ -64,4 +65,12 @@
*/
// TODO(b/122806727) Need to correctly set EXIF in JPEG images
int getRotationDegrees();
+
+ /**
+ * Adds any stored EXIF information in this ImageInfo into the provided ExifData builder.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ void populateExifData(@NonNull ExifData.Builder exifBuilder);
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImmutableImageInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/ImmutableImageInfo.java
index 1e7a45d..010bc49 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImmutableImageInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImmutableImageInfo.java
@@ -18,6 +18,7 @@
import androidx.annotation.NonNull;
import androidx.camera.core.impl.TagBundle;
+import androidx.camera.core.impl.utils.ExifData;
import com.google.auto.value.AutoValue;
@@ -37,4 +38,10 @@
@Override
public abstract int getRotationDegrees();
+
+ @Override
+ public void populateExifData(@NonNull ExifData.Builder exifBuilder) {
+ // Only have access to orientation information.
+ exifBuilder.setOrientationDegrees(getRotationDegrees());
+ }
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index e01c28d..e5f4e41 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -60,6 +60,7 @@
import androidx.annotation.experimental.UseExperimental;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraCaptureResult;
+import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.CaptureConfig;
import androidx.camera.core.impl.CaptureProcessor;
@@ -465,7 +466,8 @@
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
@Override
- UseCaseConfig<?> onMergeConfig(@NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
+ UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
+ @NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
if (builder.getMutableConfig().retrieveOption(OPTION_PREVIEW_CAPTURE_PROCESSOR, null)
!= null) {
builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.YUV_420_888);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
index ec92f42..ca32637 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
@@ -30,6 +30,7 @@
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.camera.core.impl.CameraControlInternal;
+import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.Config.Option;
@@ -164,6 +165,7 @@
/**
* Create a merged {@link UseCaseConfig} from the UseCase, camera, and an extended config.
*
+ * @param cameraInfo info about the camera which may be used to resolve conflicts.
* @param extendedConfig configs that take priority over the UseCase's default config
* @param cameraDefaultConfig configs that have lower priority than the UseCase's default.
* This Config comes from the camera implementation.
@@ -175,6 +177,7 @@
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
public UseCaseConfig<?> mergeConfigs(
+ @NonNull CameraInfoInternal cameraInfo,
@Nullable UseCaseConfig<?> extendedConfig,
@Nullable UseCaseConfig<?> cameraDefaultConfig) {
MutableOptionsBundle mergedConfig;
@@ -199,8 +202,7 @@
if (extendedConfig != null) {
// If any options need special handling, this is the place to do it. For now we'll
- // just copy
- // over all options.
+ // just copy over all options.
for (Option<?> opt : extendedConfig.listOptions()) {
@SuppressWarnings("unchecked") // Options/values are being copied directly
Option<Object> objectOpt = (Option<Object>) opt;
@@ -222,7 +224,7 @@
mergedConfig.removeOption(ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO);
}
- return onMergeConfig(getUseCaseConfigBuilder(mergedConfig));
+ return onMergeConfig(cameraInfo, getUseCaseConfigBuilder(mergedConfig));
}
/**
@@ -231,8 +233,9 @@
* <p> This can be overridden by a UseCase which need to do additional verification of the
* configs to make sure there are no conflicting options.
*
- * @param builder the builder containing the merged configs requiring addition conflict
- * resolution
+ * @param cameraInfo info about the camera which may be used to resolve conflicts.
+ * @param builder the builder containing the merged configs requiring addition conflict
+ * resolution
* @return the conflict resolved config
* @throws IllegalArgumentException if there exists conflicts in the merged config that can
* not be resolved
@@ -240,7 +243,8 @@
*/
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
- UseCaseConfig<?> onMergeConfig(@NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
+ UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
+ @NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
return builder.getUseCaseConfig();
}
@@ -263,7 +267,16 @@
UseCaseConfigUtil.updateTargetRotationAndRelatedConfigs(builder, targetRotation);
mUseCaseConfig = builder.getUseCaseConfig();
- mCurrentConfig = mergeConfigs(mExtendedConfig, mCameraConfig);
+ // Only merge configs if currently attached to a camera. Otherwise, set the current
+ // config to the use case config and mergeConfig() will be called once the use case
+ // is attached to a camera.
+ CameraInternal camera = getCamera();
+ if (camera == null) {
+ mCurrentConfig = mUseCaseConfig;
+ } else {
+ mCurrentConfig = mergeConfigs(camera.getCameraInfoInternal(), mExtendedConfig,
+ mCameraConfig);
+ }
return true;
}
@@ -531,7 +544,8 @@
mExtendedConfig = extendedConfig;
mCameraConfig = cameraConfig;
- mCurrentConfig = mergeConfigs(mExtendedConfig, mCameraConfig);
+ mCurrentConfig = mergeConfigs(camera.getCameraInfoInternal(), mExtendedConfig,
+ mCameraConfig);
EventCallback eventCallback = mCurrentConfig.getUseCaseEventCallback(null);
if (eventCallback != null) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraCaptureResult.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraCaptureResult.java
index abdc327..a808987 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraCaptureResult.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraCaptureResult.java
@@ -22,6 +22,7 @@
import androidx.camera.core.impl.CameraCaptureMetaData.AfState;
import androidx.camera.core.impl.CameraCaptureMetaData.AwbState;
import androidx.camera.core.impl.CameraCaptureMetaData.FlashState;
+import androidx.camera.core.impl.utils.ExifData;
/**
* The result of a single image capture.
@@ -60,6 +61,11 @@
@NonNull
TagBundle getTagBundle();
+ /** Populates the given Exif.Builder with attributes from this CameraCaptureResult. */
+ default void populateExifData(@NonNull ExifData.Builder exifBuilder) {
+ exifBuilder.setFlashState(getFlashState());
+ }
+
/** An implementation of CameraCaptureResult which always return default results. */
final class EmptyCameraCaptureResult implements CameraCaptureResult {
@@ -106,7 +112,7 @@
@Override
@NonNull
public TagBundle getTagBundle() {
- return null;
+ return TagBundle.emptyBundle();
}
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraFactory.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraFactory.java
index 3efbda6..1e87b19 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraFactory.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraFactory.java
@@ -40,14 +40,14 @@
*
* @param context the android context
* @param threadConfig the thread config to run the camera operations
- * @param availableCamerasSelector a CameraSelector used to specify which cameras will be
+ * @param availableCamerasLimiter a CameraSelector used to specify which cameras will be
* loaded and available to CameraX.
* @return the factory instance
* @throws InitializationException if it fails to create the factory.
*/
@NonNull CameraFactory newInstance(@NonNull Context context,
@NonNull CameraThreadConfig threadConfig,
- @Nullable CameraSelector availableCamerasSelector) throws InitializationException;
+ @Nullable CameraSelector availableCamerasLimiter) throws InitializationException;
}
/**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
index e751bc1..7dc5669 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
@@ -62,4 +62,8 @@
* {@link #addSessionCaptureCallback(Executor, CameraCaptureCallback)}.
*/
void removeSessionCaptureCallback(@NonNull CameraCaptureCallback callback);
+
+ /** Returns a list of quirks related to the camera. */
+ @NonNull
+ Quirks getCameraQuirks();
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInternal.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInternal.java
index 9c5226f..8422efc 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInternal.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInternal.java
@@ -153,10 +153,6 @@
@NonNull
CameraInfoInternal getCameraInfoInternal();
- /** Returns a list of quirks related to the camera. */
- @NonNull
- Quirks getCameraQuirks();
-
////////////////////////////////////////////////////////////////////////////////////////////////
// Camera interface
////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/Quirks.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/Quirks.java
index 2246074..5e3871f 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/Quirks.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/Quirks.java
@@ -39,6 +39,10 @@
/**
* Retrieves a {@link Quirk} instance given its type.
*
+ * <p>Unlike {@link #contains(Class)}, a quirk can only be retrieved by the exact class. If a
+ * superclass or superinterface is provided, {@code null} will be returned, even if a quirk
+ * with the provided superclass or superinterface exists in this collection.
+ *
* @param quirkClass The type of quirk to retrieve.
* @return A {@link Quirk} instance of the provided type, or {@code null} if it isn't found.
*/
@@ -52,4 +56,23 @@
}
return null;
}
+
+ /**
+ * Returns whether this collection of quirks contains a quirk with the provided type.
+ *
+ * <p>This checks whether the provided quirk type is the exact class, a superclass, or a
+ * superinterface of any of the contained quirks, and will return true in all cases.
+ * @param quirkClass The type of quirk to check for existence in this container.
+ * @return {@code true} if this container contains a quirk with the given type, {@code false}
+ * otherwise.
+ */
+ public boolean contains(@NonNull Class<? extends Quirk> quirkClass) {
+ for (Quirk quirk : mQuirks) {
+ if (quirkClass.isAssignableFrom(quirk.getClass())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ByteOrderedDataInputStream.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ByteOrderedDataInputStream.java
new file mode 100644
index 0000000..318761b
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ByteOrderedDataInputStream.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl.utils;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInput;
+import java.io.DataInputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteOrder;
+
+/**
+ * An input stream to parse EXIF data area, which can be written in either little or big endian
+ * order.
+ */
+// Note: This class is adapted from {@link androidx.exifinterface.media.ExifInterface}
+final class ByteOrderedDataInputStream extends InputStream implements DataInput {
+ private static final ByteOrder LITTLE_ENDIAN = ByteOrder.LITTLE_ENDIAN;
+ private static final ByteOrder BIG_ENDIAN = ByteOrder.BIG_ENDIAN;
+
+ private final DataInputStream mDataInputStream;
+ private ByteOrder mByteOrder = ByteOrder.BIG_ENDIAN;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final int mLength;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ int mPosition;
+
+ ByteOrderedDataInputStream(InputStream in) throws IOException {
+ this(in, ByteOrder.BIG_ENDIAN);
+ }
+
+ ByteOrderedDataInputStream(InputStream in, ByteOrder byteOrder) throws IOException {
+ mDataInputStream = new DataInputStream(in);
+ mLength = mDataInputStream.available();
+ mPosition = 0;
+ // TODO (b/142218289): Need to handle case where input stream does not support mark
+ mDataInputStream.mark(mLength);
+ mByteOrder = byteOrder;
+ }
+
+ ByteOrderedDataInputStream(byte[] bytes) throws IOException {
+ this(new ByteArrayInputStream(bytes));
+ }
+
+ public void setByteOrder(ByteOrder byteOrder) {
+ mByteOrder = byteOrder;
+ }
+
+ public void seek(long byteCount) throws IOException {
+ if (mPosition > byteCount) {
+ mPosition = 0;
+ mDataInputStream.reset();
+ // TODO (b/142218289): Need to handle case where input stream does not support mark
+ mDataInputStream.mark(mLength);
+ } else {
+ byteCount -= mPosition;
+ }
+
+ if (skipBytes((int) byteCount) != (int) byteCount) {
+ throw new IOException("Couldn't seek up to the byteCount");
+ }
+ }
+
+ public int peek() {
+ return mPosition;
+ }
+
+ @Override
+ public int available() throws IOException {
+ return mDataInputStream.available();
+ }
+
+ @Override
+ public int read() throws IOException {
+ ++mPosition;
+ return mDataInputStream.read();
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ int bytesRead = mDataInputStream.read(b, off, len);
+ mPosition += bytesRead;
+ return bytesRead;
+ }
+
+ @Override
+ public int readUnsignedByte() throws IOException {
+ ++mPosition;
+ return mDataInputStream.readUnsignedByte();
+ }
+
+ @Override
+ public String readLine() {
+ throw new UnsupportedOperationException("readLine() not implemented.");
+ }
+
+ @Override
+ public boolean readBoolean() throws IOException {
+ ++mPosition;
+ return mDataInputStream.readBoolean();
+ }
+
+ @Override
+ public char readChar() throws IOException {
+ mPosition += 2;
+ return mDataInputStream.readChar();
+ }
+
+ @Override
+ public String readUTF() throws IOException {
+ mPosition += 2;
+ return mDataInputStream.readUTF();
+ }
+
+ @Override
+ public void readFully(byte[] buffer, int offset, int length) throws IOException {
+ mPosition += length;
+ if (mPosition > mLength) {
+ throw new EOFException();
+ }
+ if (mDataInputStream.read(buffer, offset, length) != length) {
+ throw new IOException("Couldn't read up to the length of buffer");
+ }
+ }
+
+ @Override
+ public void readFully(byte[] buffer) throws IOException {
+ mPosition += buffer.length;
+ if (mPosition > mLength) {
+ throw new EOFException();
+ }
+ if (mDataInputStream.read(buffer, 0, buffer.length) != buffer.length) {
+ throw new IOException("Couldn't read up to the length of buffer");
+ }
+ }
+
+ @Override
+ public byte readByte() throws IOException {
+ ++mPosition;
+ if (mPosition > mLength) {
+ throw new EOFException();
+ }
+ int ch = mDataInputStream.read();
+ if (ch < 0) {
+ throw new EOFException();
+ }
+ return (byte) ch;
+ }
+
+ @Override
+ public short readShort() throws IOException {
+ mPosition += 2;
+ if (mPosition > mLength) {
+ throw new EOFException();
+ }
+ int ch1 = mDataInputStream.read();
+ int ch2 = mDataInputStream.read();
+ if ((ch1 | ch2) < 0) {
+ throw new EOFException();
+ }
+ if (mByteOrder == LITTLE_ENDIAN) {
+ return (short) ((ch2 << 8) + ch1);
+ } else if (mByteOrder == BIG_ENDIAN) {
+ return (short) ((ch1 << 8) + ch2);
+ }
+ throw new IOException("Invalid byte order: " + mByteOrder);
+ }
+
+ @Override
+ public int readInt() throws IOException {
+ mPosition += 4;
+ if (mPosition > mLength) {
+ throw new EOFException();
+ }
+ int ch1 = mDataInputStream.read();
+ int ch2 = mDataInputStream.read();
+ int ch3 = mDataInputStream.read();
+ int ch4 = mDataInputStream.read();
+ if ((ch1 | ch2 | ch3 | ch4) < 0) {
+ throw new EOFException();
+ }
+ if (mByteOrder == LITTLE_ENDIAN) {
+ return ((ch4 << 24) + (ch3 << 16) + (ch2 << 8) + ch1);
+ } else if (mByteOrder == BIG_ENDIAN) {
+ return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + ch4);
+ }
+ throw new IOException("Invalid byte order: " + mByteOrder);
+ }
+
+ @Override
+ public int skipBytes(int byteCount) throws IOException {
+ int totalSkip = Math.min(byteCount, mLength - mPosition);
+ int skipped = 0;
+ while (skipped < totalSkip) {
+ skipped += mDataInputStream.skipBytes(totalSkip - skipped);
+ }
+ mPosition += skipped;
+ return skipped;
+ }
+
+ @Override
+ public int readUnsignedShort() throws IOException {
+ mPosition += 2;
+ if (mPosition > mLength) {
+ throw new EOFException();
+ }
+ int ch1 = mDataInputStream.read();
+ int ch2 = mDataInputStream.read();
+ if ((ch1 | ch2) < 0) {
+ throw new EOFException();
+ }
+ if (mByteOrder == LITTLE_ENDIAN) {
+ return ((ch2 << 8) + ch1);
+ } else if (mByteOrder == BIG_ENDIAN) {
+ return ((ch1 << 8) + ch2);
+ }
+ throw new IOException("Invalid byte order: " + mByteOrder);
+ }
+
+ public long readUnsignedInt() throws IOException {
+ return readInt() & 0xffffffffL;
+ }
+
+ @Override
+ public long readLong() throws IOException {
+ mPosition += 8;
+ if (mPosition > mLength) {
+ throw new EOFException();
+ }
+ int ch1 = mDataInputStream.read();
+ int ch2 = mDataInputStream.read();
+ int ch3 = mDataInputStream.read();
+ int ch4 = mDataInputStream.read();
+ int ch5 = mDataInputStream.read();
+ int ch6 = mDataInputStream.read();
+ int ch7 = mDataInputStream.read();
+ int ch8 = mDataInputStream.read();
+ if ((ch1 | ch2 | ch3 | ch4 | ch5 | ch6 | ch7 | ch8) < 0) {
+ throw new EOFException();
+ }
+ if (mByteOrder == LITTLE_ENDIAN) {
+ return (((long) ch8 << 56) + ((long) ch7 << 48) + ((long) ch6 << 40)
+ + ((long) ch5 << 32) + ((long) ch4 << 24) + ((long) ch3 << 16)
+ + ((long) ch2 << 8) + (long) ch1);
+ } else if (mByteOrder == BIG_ENDIAN) {
+ return (((long) ch1 << 56) + ((long) ch2 << 48) + ((long) ch3 << 40)
+ + ((long) ch4 << 32) + ((long) ch5 << 24) + ((long) ch6 << 16)
+ + ((long) ch7 << 8) + (long) ch8);
+ }
+ throw new IOException("Invalid byte order: " + mByteOrder);
+ }
+
+ @Override
+ public float readFloat() throws IOException {
+ return Float.intBitsToFloat(readInt());
+ }
+
+ @Override
+ public double readDouble() throws IOException {
+ return Double.longBitsToDouble(readLong());
+ }
+
+ @Override
+ public void mark(int readlimit) {
+ synchronized (mDataInputStream) {
+ mDataInputStream.mark(readlimit);
+ }
+ }
+
+ public int getLength() {
+ return mLength;
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ByteOrderedDataOutputStream.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ByteOrderedDataOutputStream.java
new file mode 100644
index 0000000..5f830f8
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ByteOrderedDataOutputStream.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl.utils;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteOrder;
+
+/**
+ * An output stream to write EXIF data area, which can be written in either little or big endian
+ * order.
+ */
+// Note: This class is adapted from {@link androidx.exifinterface.media.ExifInterface}
+class ByteOrderedDataOutputStream extends FilterOutputStream {
+ final OutputStream mOutputStream;
+ private ByteOrder mByteOrder;
+
+ ByteOrderedDataOutputStream(OutputStream out, ByteOrder byteOrder) {
+ super(out);
+ mOutputStream = out;
+ mByteOrder = byteOrder;
+ }
+
+ public void setByteOrder(ByteOrder byteOrder) {
+ mByteOrder = byteOrder;
+ }
+
+ @Override
+ public void write(byte[] bytes) throws IOException {
+ mOutputStream.write(bytes);
+ }
+
+ @Override
+ public void write(byte[] bytes, int offset, int length) throws IOException {
+ mOutputStream.write(bytes, offset, length);
+ }
+
+ public void writeByte(int val) throws IOException {
+ mOutputStream.write(val);
+ }
+
+ public void writeShort(short val) throws IOException {
+ if (mByteOrder == ByteOrder.LITTLE_ENDIAN) {
+ mOutputStream.write((val >>> 0) & 0xFF);
+ mOutputStream.write((val >>> 8) & 0xFF);
+ } else if (mByteOrder == ByteOrder.BIG_ENDIAN) {
+ mOutputStream.write((val >>> 8) & 0xFF);
+ mOutputStream.write((val >>> 0) & 0xFF);
+ }
+ }
+
+ public void writeInt(int val) throws IOException {
+ if (mByteOrder == ByteOrder.LITTLE_ENDIAN) {
+ mOutputStream.write((val >>> 0) & 0xFF);
+ mOutputStream.write((val >>> 8) & 0xFF);
+ mOutputStream.write((val >>> 16) & 0xFF);
+ mOutputStream.write((val >>> 24) & 0xFF);
+ } else if (mByteOrder == ByteOrder.BIG_ENDIAN) {
+ mOutputStream.write((val >>> 24) & 0xFF);
+ mOutputStream.write((val >>> 16) & 0xFF);
+ mOutputStream.write((val >>> 8) & 0xFF);
+ mOutputStream.write((val >>> 0) & 0xFF);
+ }
+ }
+
+ public void writeUnsignedShort(int val) throws IOException {
+ writeShort((short) val);
+ }
+
+ public void writeUnsignedInt(long val) throws IOException {
+ writeInt((int) val);
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifAttribute.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifAttribute.java
new file mode 100644
index 0000000..02a1d39
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifAttribute.java
@@ -0,0 +1,462 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl.utils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.Logger;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * A class for indicating EXIF attribute.
+ *
+ * This class was pulled from the {@link androidx.exifinterface.media.ExifInterface} class.
+ */
+final class ExifAttribute {
+ private static final String TAG = "ExifAttribute";
+ public static final long BYTES_OFFSET_UNKNOWN = -1;
+
+ // See JPEG File Interchange Format Version 1.02.
+ // The following values are defined for handling JPEG streams. In this implementation, we are
+ // not only getting information from EXIF but also from some JPEG special segments such as
+ // MARKER_COM for user comment and MARKER_SOFx for image width and height.
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ static final Charset ASCII = StandardCharsets.US_ASCII;
+
+ // Formats for the value in IFD entry (See TIFF 6.0 Section 2, "Image File Directory".)
+ static final int IFD_FORMAT_BYTE = 1;
+ static final int IFD_FORMAT_STRING = 2;
+ static final int IFD_FORMAT_USHORT = 3;
+ static final int IFD_FORMAT_ULONG = 4;
+ static final int IFD_FORMAT_URATIONAL = 5;
+ static final int IFD_FORMAT_SBYTE = 6;
+ static final int IFD_FORMAT_UNDEFINED = 7;
+ static final int IFD_FORMAT_SSHORT = 8;
+ static final int IFD_FORMAT_SLONG = 9;
+ static final int IFD_FORMAT_SRATIONAL = 10;
+ static final int IFD_FORMAT_SINGLE = 11;
+ static final int IFD_FORMAT_DOUBLE = 12;
+ // Names for the data formats for debugging purpose.
+ static final String[] IFD_FORMAT_NAMES = new String[] {
+ "", "BYTE", "STRING", "USHORT", "ULONG", "URATIONAL", "SBYTE", "UNDEFINED", "SSHORT",
+ "SLONG", "SRATIONAL", "SINGLE", "DOUBLE", "IFD"
+ };
+ // Sizes of the components of each IFD value format
+ static final int[] IFD_FORMAT_BYTES_PER_FORMAT = new int[] {
+ 0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8, 1
+ };
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ static final byte[] EXIF_ASCII_PREFIX = new byte[] {
+ 0x41, 0x53, 0x43, 0x49, 0x49, 0x0, 0x0, 0x0
+ };
+
+ public final int format;
+ public final int numberOfComponents;
+ public final long bytesOffset;
+ public final byte[] bytes;
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ ExifAttribute(int format, int numberOfComponents, byte[] bytes) {
+ this(format, numberOfComponents, BYTES_OFFSET_UNKNOWN, bytes);
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ ExifAttribute(int format, int numberOfComponents, long bytesOffset, byte[] bytes) {
+ this.format = format;
+ this.numberOfComponents = numberOfComponents;
+ this.bytesOffset = bytesOffset;
+ this.bytes = bytes;
+ }
+
+ @NonNull
+ public static ExifAttribute createUShort(@NonNull int[] values, @NonNull ByteOrder byteOrder) {
+ final ByteBuffer buffer = ByteBuffer.wrap(
+ new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_USHORT] * values.length]);
+ buffer.order(byteOrder);
+ for (int value : values) {
+ buffer.putShort((short) value);
+ }
+ return new ExifAttribute(IFD_FORMAT_USHORT, values.length, buffer.array());
+ }
+
+ @NonNull
+ public static ExifAttribute createUShort(int value, @NonNull ByteOrder byteOrder) {
+ return createUShort(new int[] {value}, byteOrder);
+ }
+
+ @NonNull
+ public static ExifAttribute createULong(@NonNull long[] values, @NonNull ByteOrder byteOrder) {
+ final ByteBuffer buffer = ByteBuffer.wrap(
+ new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_ULONG] * values.length]);
+ buffer.order(byteOrder);
+ for (long value : values) {
+ buffer.putInt((int) value);
+ }
+ return new ExifAttribute(IFD_FORMAT_ULONG, values.length, buffer.array());
+ }
+
+ @NonNull
+ public static ExifAttribute createULong(long value, @NonNull ByteOrder byteOrder) {
+ return createULong(new long[] {value}, byteOrder);
+ }
+
+ @NonNull
+ public static ExifAttribute createSLong(@NonNull int[] values, @NonNull ByteOrder byteOrder) {
+ final ByteBuffer buffer = ByteBuffer.wrap(
+ new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_SLONG] * values.length]);
+ buffer.order(byteOrder);
+ for (int value : values) {
+ buffer.putInt(value);
+ }
+ return new ExifAttribute(IFD_FORMAT_SLONG, values.length, buffer.array());
+ }
+
+ @NonNull
+ public static ExifAttribute createSLong(int value, @NonNull ByteOrder byteOrder) {
+ return createSLong(new int[] {value}, byteOrder);
+ }
+
+ @NonNull
+ public static ExifAttribute createByte(@NonNull String value) {
+ // Exception for GPSAltitudeRef tag
+ if (value.length() == 1 && value.charAt(0) >= '0' && value.charAt(0) <= '1') {
+ final byte[] bytes = new byte[] { (byte) (value.charAt(0) - '0') };
+ return new ExifAttribute(IFD_FORMAT_BYTE, bytes.length, bytes);
+ }
+ final byte[] ascii = value.getBytes(ASCII);
+ return new ExifAttribute(IFD_FORMAT_BYTE, ascii.length, ascii);
+ }
+
+ @NonNull
+ public static ExifAttribute createString(@NonNull String value) {
+ final byte[] ascii = (value + '\0').getBytes(ASCII);
+ return new ExifAttribute(IFD_FORMAT_STRING, ascii.length, ascii);
+ }
+
+ @NonNull
+ public static ExifAttribute createURational(@NonNull LongRational[] values,
+ @NonNull ByteOrder byteOrder) {
+ final ByteBuffer buffer = ByteBuffer.wrap(
+ new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_URATIONAL] * values.length]);
+ buffer.order(byteOrder);
+ for (LongRational value : values) {
+ buffer.putInt((int) value.getNumerator());
+ buffer.putInt((int) value.getDenominator());
+ }
+ return new ExifAttribute(IFD_FORMAT_URATIONAL, values.length, buffer.array());
+ }
+
+ @NonNull
+ public static ExifAttribute createURational(@NonNull LongRational value,
+ @NonNull ByteOrder byteOrder) {
+ return createURational(new LongRational[] {value}, byteOrder);
+ }
+
+ @NonNull
+ public static ExifAttribute createSRational(@NonNull LongRational[] values,
+ @NonNull ByteOrder byteOrder) {
+ final ByteBuffer buffer = ByteBuffer.wrap(
+ new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_SRATIONAL] * values.length]);
+ buffer.order(byteOrder);
+ for (LongRational value : values) {
+ buffer.putInt((int) value.getNumerator());
+ buffer.putInt((int) value.getDenominator());
+ }
+ return new ExifAttribute(IFD_FORMAT_SRATIONAL, values.length, buffer.array());
+ }
+
+ @NonNull
+ public static ExifAttribute createSRational(@NonNull LongRational value,
+ @NonNull ByteOrder byteOrder) {
+ return createSRational(new LongRational[] {value}, byteOrder);
+ }
+
+ @NonNull
+ public static ExifAttribute createDouble(@NonNull double[] values,
+ @NonNull ByteOrder byteOrder) {
+ final ByteBuffer buffer = ByteBuffer.wrap(
+ new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_DOUBLE] * values.length]);
+ buffer.order(byteOrder);
+ for (double value : values) {
+ buffer.putDouble(value);
+ }
+ return new ExifAttribute(IFD_FORMAT_DOUBLE, values.length, buffer.array());
+ }
+
+ @NonNull
+ public static ExifAttribute createDouble(double value, @NonNull ByteOrder byteOrder) {
+ return createDouble(new double[] {value}, byteOrder);
+ }
+
+ @Override
+ public String toString() {
+ return "(" + IFD_FORMAT_NAMES[format] + ", data length:" + bytes.length + ")";
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ Object getValue(ByteOrder byteOrder) {
+ ByteOrderedDataInputStream inputStream = null;
+ try {
+ inputStream = new ByteOrderedDataInputStream(bytes);
+ inputStream.setByteOrder(byteOrder);
+ switch (format) {
+ case IFD_FORMAT_BYTE:
+ case IFD_FORMAT_SBYTE: {
+ // Exception for GPSAltitudeRef tag
+ if (bytes.length == 1 && bytes[0] >= 0 && bytes[0] <= 1) {
+ return new String(new char[] { (char) (bytes[0] + '0') });
+ }
+ return new String(bytes, ASCII);
+ }
+ case IFD_FORMAT_UNDEFINED:
+ case IFD_FORMAT_STRING: {
+ int index = 0;
+ if (numberOfComponents >= EXIF_ASCII_PREFIX.length) {
+ boolean same = true;
+ for (int i = 0; i < EXIF_ASCII_PREFIX.length; ++i) {
+ if (bytes[i] != EXIF_ASCII_PREFIX[i]) {
+ same = false;
+ break;
+ }
+ }
+ if (same) {
+ index = EXIF_ASCII_PREFIX.length;
+ }
+ }
+
+ StringBuilder stringBuilder = new StringBuilder();
+ while (index < numberOfComponents) {
+ int ch = bytes[index];
+ if (ch == 0) {
+ break;
+ }
+ if (ch >= 32) {
+ stringBuilder.append((char) ch);
+ } else {
+ stringBuilder.append('?');
+ }
+ ++index;
+ }
+ return stringBuilder.toString();
+ }
+ case IFD_FORMAT_USHORT: {
+ final int[] values = new int[numberOfComponents];
+ for (int i = 0; i < numberOfComponents; ++i) {
+ values[i] = inputStream.readUnsignedShort();
+ }
+ return values;
+ }
+ case IFD_FORMAT_ULONG: {
+ final long[] values = new long[numberOfComponents];
+ for (int i = 0; i < numberOfComponents; ++i) {
+ values[i] = inputStream.readUnsignedInt();
+ }
+ return values;
+ }
+ case IFD_FORMAT_URATIONAL: {
+ final LongRational[] values = new LongRational[numberOfComponents];
+ for (int i = 0; i < numberOfComponents; ++i) {
+ final long numerator = inputStream.readUnsignedInt();
+ final long denominator = inputStream.readUnsignedInt();
+ values[i] = new LongRational(numerator, denominator);
+ }
+ return values;
+ }
+ case IFD_FORMAT_SSHORT: {
+ final int[] values = new int[numberOfComponents];
+ for (int i = 0; i < numberOfComponents; ++i) {
+ values[i] = inputStream.readShort();
+ }
+ return values;
+ }
+ case IFD_FORMAT_SLONG: {
+ final int[] values = new int[numberOfComponents];
+ for (int i = 0; i < numberOfComponents; ++i) {
+ values[i] = inputStream.readInt();
+ }
+ return values;
+ }
+ case IFD_FORMAT_SRATIONAL: {
+ final LongRational[] values = new LongRational[numberOfComponents];
+ for (int i = 0; i < numberOfComponents; ++i) {
+ final long numerator = inputStream.readInt();
+ final long denominator = inputStream.readInt();
+ values[i] = new LongRational(numerator, denominator);
+ }
+ return values;
+ }
+ case IFD_FORMAT_SINGLE: {
+ final double[] values = new double[numberOfComponents];
+ for (int i = 0; i < numberOfComponents; ++i) {
+ values[i] = inputStream.readFloat();
+ }
+ return values;
+ }
+ case IFD_FORMAT_DOUBLE: {
+ final double[] values = new double[numberOfComponents];
+ for (int i = 0; i < numberOfComponents; ++i) {
+ values[i] = inputStream.readDouble();
+ }
+ return values;
+ }
+ default:
+ return null;
+ }
+ } catch (IOException e) {
+ Logger.w(TAG, "IOException occurred during reading a value", e);
+ return null;
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ Logger.e(TAG, "IOException occurred while closing InputStream", e);
+ }
+ }
+ }
+ }
+
+ public double getDoubleValue(@NonNull ByteOrder byteOrder) {
+ Object value = getValue(byteOrder);
+ if (value == null) {
+ throw new NumberFormatException("NULL can't be converted to a double value");
+ }
+ if (value instanceof String) {
+ return Double.parseDouble((String) value);
+ }
+ if (value instanceof long[]) {
+ long[] array = (long[]) value;
+ if (array.length == 1) {
+ return array[0];
+ }
+ throw new NumberFormatException("There are more than one component");
+ }
+ if (value instanceof int[]) {
+ int[] array = (int[]) value;
+ if (array.length == 1) {
+ return array[0];
+ }
+ throw new NumberFormatException("There are more than one component");
+ }
+ if (value instanceof double[]) {
+ double[] array = (double[]) value;
+ if (array.length == 1) {
+ return array[0];
+ }
+ throw new NumberFormatException("There are more than one component");
+ }
+ if (value instanceof LongRational[]) {
+ LongRational[] array = (LongRational[]) value;
+ if (array.length == 1) {
+ return array[0].toDouble();
+ }
+ throw new NumberFormatException("There are more than one component");
+ }
+ throw new NumberFormatException("Couldn't find a double value");
+ }
+
+ public int getIntValue(@NonNull ByteOrder byteOrder) {
+ Object value = getValue(byteOrder);
+ if (value == null) {
+ throw new NumberFormatException("NULL can't be converted to a integer value");
+ }
+ if (value instanceof String) {
+ return Integer.parseInt((String) value);
+ }
+ if (value instanceof long[]) {
+ long[] array = (long[]) value;
+ if (array.length == 1) {
+ return (int) array[0];
+ }
+ throw new NumberFormatException("There are more than one component");
+ }
+ if (value instanceof int[]) {
+ int[] array = (int[]) value;
+ if (array.length == 1) {
+ return array[0];
+ }
+ throw new NumberFormatException("There are more than one component");
+ }
+ throw new NumberFormatException("Couldn't find a integer value");
+ }
+
+ @Nullable
+ public String getStringValue(@NonNull ByteOrder byteOrder) {
+ Object value = getValue(byteOrder);
+ if (value == null) {
+ return null;
+ }
+ if (value instanceof String) {
+ return (String) value;
+ }
+
+ final StringBuilder stringBuilder = new StringBuilder();
+ if (value instanceof long[]) {
+ long[] array = (long[]) value;
+ for (int i = 0; i < array.length; ++i) {
+ stringBuilder.append(array[i]);
+ if (i + 1 != array.length) {
+ stringBuilder.append(",");
+ }
+ }
+ return stringBuilder.toString();
+ }
+ if (value instanceof int[]) {
+ int[] array = (int[]) value;
+ for (int i = 0; i < array.length; ++i) {
+ stringBuilder.append(array[i]);
+ if (i + 1 != array.length) {
+ stringBuilder.append(",");
+ }
+ }
+ return stringBuilder.toString();
+ }
+ if (value instanceof double[]) {
+ double[] array = (double[]) value;
+ for (int i = 0; i < array.length; ++i) {
+ stringBuilder.append(array[i]);
+ if (i + 1 != array.length) {
+ stringBuilder.append(",");
+ }
+ }
+ return stringBuilder.toString();
+ }
+ if (value instanceof LongRational[]) {
+ LongRational[] array = (LongRational[]) value;
+ for (int i = 0; i < array.length; ++i) {
+ stringBuilder.append(array[i].getNumerator());
+ stringBuilder.append('/');
+ stringBuilder.append(array[i].getDenominator());
+ if (i + 1 != array.length) {
+ stringBuilder.append(",");
+ }
+ }
+ return stringBuilder.toString();
+ }
+ return null;
+ }
+
+ public int size() {
+ return IFD_FORMAT_BYTES_PER_FORMAT[format] * numberOfComponents;
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java
new file mode 100644
index 0000000..ea5d08a
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifData.java
@@ -0,0 +1,967 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl.utils;
+
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_BYTE;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_DOUBLE;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_SLONG;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_SRATIONAL;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_STRING;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_ULONG;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_UNDEFINED;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_URATIONAL;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_USHORT;
+import static androidx.exifinterface.media.ExifInterface.CONTRAST_NORMAL;
+import static androidx.exifinterface.media.ExifInterface.EXPOSURE_PROGRAM_NOT_DEFINED;
+import static androidx.exifinterface.media.ExifInterface.FILE_SOURCE_DSC;
+import static androidx.exifinterface.media.ExifInterface.FLAG_FLASH_FIRED;
+import static androidx.exifinterface.media.ExifInterface.FLAG_FLASH_NO_FLASH_FUNCTION;
+import static androidx.exifinterface.media.ExifInterface.GPS_DIRECTION_TRUE;
+import static androidx.exifinterface.media.ExifInterface.GPS_DISTANCE_KILOMETERS;
+import static androidx.exifinterface.media.ExifInterface.GPS_SPEED_KILOMETERS_PER_HOUR;
+import static androidx.exifinterface.media.ExifInterface.LIGHT_SOURCE_FLASH;
+import static androidx.exifinterface.media.ExifInterface.LIGHT_SOURCE_UNKNOWN;
+import static androidx.exifinterface.media.ExifInterface.METERING_MODE_UNKNOWN;
+import static androidx.exifinterface.media.ExifInterface.ORIENTATION_NORMAL;
+import static androidx.exifinterface.media.ExifInterface.RENDERED_PROCESS_NORMAL;
+import static androidx.exifinterface.media.ExifInterface.RESOLUTION_UNIT_INCHES;
+import static androidx.exifinterface.media.ExifInterface.SATURATION_NORMAL;
+import static androidx.exifinterface.media.ExifInterface.SCENE_CAPTURE_TYPE_STANDARD;
+import static androidx.exifinterface.media.ExifInterface.SCENE_TYPE_DIRECTLY_PHOTOGRAPHED;
+import static androidx.exifinterface.media.ExifInterface.SENSITIVITY_TYPE_ISO_SPEED;
+import static androidx.exifinterface.media.ExifInterface.SHARPNESS_NORMAL;
+import static androidx.exifinterface.media.ExifInterface.TAG_APERTURE_VALUE;
+import static androidx.exifinterface.media.ExifInterface.TAG_BRIGHTNESS_VALUE;
+import static androidx.exifinterface.media.ExifInterface.TAG_COLOR_SPACE;
+import static androidx.exifinterface.media.ExifInterface.TAG_COMPONENTS_CONFIGURATION;
+import static androidx.exifinterface.media.ExifInterface.TAG_CONTRAST;
+import static androidx.exifinterface.media.ExifInterface.TAG_CUSTOM_RENDERED;
+import static androidx.exifinterface.media.ExifInterface.TAG_DATETIME;
+import static androidx.exifinterface.media.ExifInterface.TAG_DATETIME_DIGITIZED;
+import static androidx.exifinterface.media.ExifInterface.TAG_DATETIME_ORIGINAL;
+import static androidx.exifinterface.media.ExifInterface.TAG_EXIF_VERSION;
+import static androidx.exifinterface.media.ExifInterface.TAG_EXPOSURE_BIAS_VALUE;
+import static androidx.exifinterface.media.ExifInterface.TAG_EXPOSURE_MODE;
+import static androidx.exifinterface.media.ExifInterface.TAG_EXPOSURE_PROGRAM;
+import static androidx.exifinterface.media.ExifInterface.TAG_EXPOSURE_TIME;
+import static androidx.exifinterface.media.ExifInterface.TAG_FILE_SOURCE;
+import static androidx.exifinterface.media.ExifInterface.TAG_FLASH;
+import static androidx.exifinterface.media.ExifInterface.TAG_FLASHPIX_VERSION;
+import static androidx.exifinterface.media.ExifInterface.TAG_FOCAL_LENGTH;
+import static androidx.exifinterface.media.ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT;
+import static androidx.exifinterface.media.ExifInterface.TAG_F_NUMBER;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_ALTITUDE;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_ALTITUDE_REF;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_DEST_BEARING_REF;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_DEST_DISTANCE_REF;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_IMG_DIRECTION_REF;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_LATITUDE;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_LATITUDE_REF;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_LONGITUDE;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_LONGITUDE_REF;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_SPEED_REF;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_TIMESTAMP;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_TRACK_REF;
+import static androidx.exifinterface.media.ExifInterface.TAG_GPS_VERSION_ID;
+import static androidx.exifinterface.media.ExifInterface.TAG_IMAGE_LENGTH;
+import static androidx.exifinterface.media.ExifInterface.TAG_IMAGE_WIDTH;
+import static androidx.exifinterface.media.ExifInterface.TAG_INTEROPERABILITY_INDEX;
+import static androidx.exifinterface.media.ExifInterface.TAG_ISO_SPEED_RATINGS;
+import static androidx.exifinterface.media.ExifInterface.TAG_LIGHT_SOURCE;
+import static androidx.exifinterface.media.ExifInterface.TAG_MAKE;
+import static androidx.exifinterface.media.ExifInterface.TAG_MAX_APERTURE_VALUE;
+import static androidx.exifinterface.media.ExifInterface.TAG_METERING_MODE;
+import static androidx.exifinterface.media.ExifInterface.TAG_MODEL;
+import static androidx.exifinterface.media.ExifInterface.TAG_ORIENTATION;
+import static androidx.exifinterface.media.ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY;
+import static androidx.exifinterface.media.ExifInterface.TAG_PIXEL_X_DIMENSION;
+import static androidx.exifinterface.media.ExifInterface.TAG_PIXEL_Y_DIMENSION;
+import static androidx.exifinterface.media.ExifInterface.TAG_RESOLUTION_UNIT;
+import static androidx.exifinterface.media.ExifInterface.TAG_SATURATION;
+import static androidx.exifinterface.media.ExifInterface.TAG_SCENE_CAPTURE_TYPE;
+import static androidx.exifinterface.media.ExifInterface.TAG_SCENE_TYPE;
+import static androidx.exifinterface.media.ExifInterface.TAG_SENSING_METHOD;
+import static androidx.exifinterface.media.ExifInterface.TAG_SENSITIVITY_TYPE;
+import static androidx.exifinterface.media.ExifInterface.TAG_SHARPNESS;
+import static androidx.exifinterface.media.ExifInterface.TAG_SHUTTER_SPEED_VALUE;
+import static androidx.exifinterface.media.ExifInterface.TAG_SOFTWARE;
+import static androidx.exifinterface.media.ExifInterface.TAG_SUBSEC_TIME;
+import static androidx.exifinterface.media.ExifInterface.TAG_SUBSEC_TIME_DIGITIZED;
+import static androidx.exifinterface.media.ExifInterface.TAG_SUBSEC_TIME_ORIGINAL;
+import static androidx.exifinterface.media.ExifInterface.TAG_WHITE_BALANCE;
+import static androidx.exifinterface.media.ExifInterface.TAG_X_RESOLUTION;
+import static androidx.exifinterface.media.ExifInterface.TAG_Y_CB_CR_POSITIONING;
+import static androidx.exifinterface.media.ExifInterface.TAG_Y_RESOLUTION;
+import static androidx.exifinterface.media.ExifInterface.WHITE_BALANCE_AUTO;
+import static androidx.exifinterface.media.ExifInterface.WHITE_BALANCE_MANUAL;
+import static androidx.exifinterface.media.ExifInterface.Y_CB_CR_POSITIONING_CENTERED;
+
+import android.os.Build;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.CameraCaptureMetaData;
+import androidx.core.util.Preconditions;
+import androidx.exifinterface.media.ExifInterface;
+
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This class stores the EXIF header in IFDs according to the JPEG specification.
+ */
+// Note: This class is adapted from {@link androidx.exifinterface.media.ExifInterface}, and is
+// currently expected to be used for writing a subset of Exif values. Support for other mime
+// types besides JPEG have been removed. Support for thumbnails/strips has been removed along
+// with many exif tags. If more tags are required, the source code for ExifInterface should be
+// referenced and can be adapted to this class.
+public class ExifData {
+ private static final String TAG = "ExifData";
+ private static final boolean DEBUG = false;
+
+ /**
+ * Enum representing the white balance mode.
+ */
+ public enum WhiteBalanceMode {
+ /** AWB is turned on. */
+ AUTO,
+ /** AWB is turned off. */
+ MANUAL
+ }
+
+ // Names for the data formats for debugging purpose.
+ static final String[] IFD_FORMAT_NAMES = new String[]{
+ "", "BYTE", "STRING", "USHORT", "ULONG", "URATIONAL", "SBYTE", "UNDEFINED", "SSHORT",
+ "SLONG", "SRATIONAL", "SINGLE", "DOUBLE", "IFD"
+ };
+
+ /**
+ * Private tags used for pointing the other IFD offsets.
+ * The types of the following tags are int.
+ * See JEITA CP-3451C Section 4.6.3: Exif-specific IFD.
+ * For SubIFD, see Note 1 of Adobe PageMaker® 6.0 TIFF Technical Notes.
+ */
+ static final String TAG_EXIF_IFD_POINTER = "ExifIFDPointer";
+ static final String TAG_GPS_INFO_IFD_POINTER = "GPSInfoIFDPointer";
+ static final String TAG_INTEROPERABILITY_IFD_POINTER = "InteroperabilityIFDPointer";
+ static final String TAG_SUB_IFD_POINTER = "SubIFDPointer";
+
+ // Primary image IFD TIFF tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
+ // This is only a subset of the tags defined in ExifInterface
+ private static final ExifTag[] IFD_TIFF_TAGS = new ExifTag[]{
+ // For below two, see TIFF 6.0 Spec Section 3: Bilevel Images.
+ new ExifTag(TAG_IMAGE_WIDTH, 256, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
+ new ExifTag(TAG_IMAGE_LENGTH, 257, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
+ new ExifTag(TAG_MAKE, 271, IFD_FORMAT_STRING),
+ new ExifTag(TAG_MODEL, 272, IFD_FORMAT_STRING),
+ new ExifTag(TAG_ORIENTATION, 274, IFD_FORMAT_USHORT),
+ new ExifTag(TAG_X_RESOLUTION, 282, IFD_FORMAT_URATIONAL),
+ new ExifTag(TAG_Y_RESOLUTION, 283, IFD_FORMAT_URATIONAL),
+ new ExifTag(TAG_RESOLUTION_UNIT, 296, IFD_FORMAT_USHORT),
+ new ExifTag(TAG_SOFTWARE, 305, IFD_FORMAT_STRING),
+ new ExifTag(TAG_DATETIME, 306, IFD_FORMAT_STRING),
+ new ExifTag(TAG_Y_CB_CR_POSITIONING, 531, IFD_FORMAT_USHORT),
+ new ExifTag(TAG_SUB_IFD_POINTER, 330, IFD_FORMAT_ULONG),
+ new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG),
+ new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG),
+ };
+
+ // Primary image IFD Exif Private tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
+ // This is only a subset of the tags defined in ExifInterface
+ private static final ExifTag[] IFD_EXIF_TAGS = new ExifTag[]{
+ new ExifTag(TAG_EXPOSURE_TIME, 33434, IFD_FORMAT_URATIONAL),
+ new ExifTag(TAG_F_NUMBER, 33437, IFD_FORMAT_URATIONAL),
+ new ExifTag(TAG_EXPOSURE_PROGRAM, 34850, IFD_FORMAT_USHORT),
+ new ExifTag(TAG_PHOTOGRAPHIC_SENSITIVITY, 34855, IFD_FORMAT_USHORT),
+ new ExifTag(TAG_SENSITIVITY_TYPE, 34864, IFD_FORMAT_USHORT),
+ new ExifTag(TAG_EXIF_VERSION, 36864, IFD_FORMAT_STRING),
+ new ExifTag(TAG_DATETIME_ORIGINAL, 36867, IFD_FORMAT_STRING),
+ new ExifTag(TAG_DATETIME_DIGITIZED, 36868, IFD_FORMAT_STRING),
+ new ExifTag(TAG_COMPONENTS_CONFIGURATION, 37121, IFD_FORMAT_UNDEFINED),
+ new ExifTag(TAG_SHUTTER_SPEED_VALUE, 37377, IFD_FORMAT_SRATIONAL),
+ new ExifTag(TAG_APERTURE_VALUE, 37378, IFD_FORMAT_URATIONAL),
+ new ExifTag(TAG_BRIGHTNESS_VALUE, 37379, IFD_FORMAT_SRATIONAL),
+ new ExifTag(TAG_EXPOSURE_BIAS_VALUE, 37380, IFD_FORMAT_SRATIONAL),
+ new ExifTag(TAG_MAX_APERTURE_VALUE, 37381, IFD_FORMAT_URATIONAL),
+ new ExifTag(TAG_METERING_MODE, 37383, IFD_FORMAT_USHORT),
+ new ExifTag(TAG_LIGHT_SOURCE, 37384, IFD_FORMAT_USHORT),
+ new ExifTag(TAG_FLASH, 37385, IFD_FORMAT_USHORT),
+ new ExifTag(TAG_FOCAL_LENGTH, 37386, IFD_FORMAT_URATIONAL),
+ new ExifTag(TAG_SUBSEC_TIME, 37520, IFD_FORMAT_STRING),
+ new ExifTag(TAG_SUBSEC_TIME_ORIGINAL, 37521, IFD_FORMAT_STRING),
+ new ExifTag(TAG_SUBSEC_TIME_DIGITIZED, 37522, IFD_FORMAT_STRING),
+ new ExifTag(TAG_FLASHPIX_VERSION, 40960, IFD_FORMAT_UNDEFINED),
+ new ExifTag(TAG_COLOR_SPACE, 40961, IFD_FORMAT_USHORT),
+ new ExifTag(TAG_PIXEL_X_DIMENSION, 40962, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
+ new ExifTag(TAG_PIXEL_Y_DIMENSION, 40963, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
+ new ExifTag(TAG_INTEROPERABILITY_IFD_POINTER, 40965, IFD_FORMAT_ULONG),
+ new ExifTag(TAG_FOCAL_PLANE_RESOLUTION_UNIT, 41488, IFD_FORMAT_USHORT),
+ new ExifTag(TAG_SENSING_METHOD, 41495, IFD_FORMAT_USHORT),
+ new ExifTag(TAG_FILE_SOURCE, 41728, IFD_FORMAT_UNDEFINED),
+ new ExifTag(TAG_SCENE_TYPE, 41729, IFD_FORMAT_UNDEFINED),
+ new ExifTag(TAG_CUSTOM_RENDERED, 41985, IFD_FORMAT_USHORT),
+ new ExifTag(TAG_EXPOSURE_MODE, 41986, IFD_FORMAT_USHORT),
+ new ExifTag(TAG_WHITE_BALANCE, 41987, IFD_FORMAT_USHORT),
+ new ExifTag(TAG_SCENE_CAPTURE_TYPE, 41990, IFD_FORMAT_USHORT),
+ new ExifTag(TAG_CONTRAST, 41992, IFD_FORMAT_USHORT),
+ new ExifTag(TAG_SATURATION, 41993, IFD_FORMAT_USHORT),
+ new ExifTag(TAG_SHARPNESS, 41994, IFD_FORMAT_USHORT)
+ };
+
+ // Primary image IFD GPS Info tags (See JEITA CP-3451C Section 4.6.6 Tag Support Levels)
+ // This is only a subset of the tags defined in ExifInterface
+ private static final ExifTag[] IFD_GPS_TAGS = new ExifTag[]{
+ new ExifTag(TAG_GPS_VERSION_ID, 0, IFD_FORMAT_BYTE),
+ new ExifTag(TAG_GPS_LATITUDE_REF, 1, IFD_FORMAT_STRING),
+ // Allow SRATIONAL to be compatible with apps using wrong format and
+ // even if it is negative, it may be valid latitude / longitude.
+ new ExifTag(TAG_GPS_LATITUDE, 2, IFD_FORMAT_URATIONAL, IFD_FORMAT_SRATIONAL),
+ new ExifTag(TAG_GPS_LONGITUDE_REF, 3, IFD_FORMAT_STRING),
+ new ExifTag(TAG_GPS_LONGITUDE, 4, IFD_FORMAT_URATIONAL, IFD_FORMAT_SRATIONAL),
+ new ExifTag(TAG_GPS_ALTITUDE_REF, 5, IFD_FORMAT_BYTE),
+ new ExifTag(TAG_GPS_ALTITUDE, 6, IFD_FORMAT_URATIONAL),
+ new ExifTag(TAG_GPS_TIMESTAMP, 7, IFD_FORMAT_URATIONAL),
+ new ExifTag(TAG_GPS_SPEED_REF, 12, IFD_FORMAT_STRING),
+ new ExifTag(TAG_GPS_TRACK_REF, 14, IFD_FORMAT_STRING),
+ new ExifTag(TAG_GPS_IMG_DIRECTION_REF, 16, IFD_FORMAT_STRING),
+ new ExifTag(TAG_GPS_DEST_BEARING_REF, 23, IFD_FORMAT_STRING),
+ new ExifTag(TAG_GPS_DEST_DISTANCE_REF, 25, IFD_FORMAT_STRING)
+ };
+
+ // List of tags for pointing to the other image file directory offset.
+ static final ExifTag[] EXIF_POINTER_TAGS = new ExifTag[]{
+ new ExifTag(TAG_SUB_IFD_POINTER, 330, IFD_FORMAT_ULONG),
+ new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG),
+ new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG),
+ new ExifTag(TAG_INTEROPERABILITY_IFD_POINTER, 40965, IFD_FORMAT_ULONG),
+ };
+
+ // Primary image IFD Interoperability tag (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
+ private static final ExifTag[] IFD_INTEROPERABILITY_TAGS = new ExifTag[]{
+ new ExifTag(TAG_INTEROPERABILITY_INDEX, 1, IFD_FORMAT_STRING)
+ };
+
+ // List of Exif tag groups
+ static final ExifTag[][] EXIF_TAGS = new ExifTag[][]{
+ IFD_TIFF_TAGS, IFD_EXIF_TAGS, IFD_GPS_TAGS, IFD_INTEROPERABILITY_TAGS
+ };
+
+ // Indices for the above tags. Note these must stay in sync with the order of EXIF_TAGS.
+ static final int IFD_TYPE_PRIMARY = 0;
+ static final int IFD_TYPE_EXIF = 1;
+ static final int IFD_TYPE_GPS = 2;
+ static final int IFD_TYPE_INTEROPERABILITY = 3;
+
+ // NOTE: This is a subset of the tags from ExifInterface. Only supports tags in this class.
+ static final HashSet<String> sTagSetForCompatibility = new HashSet<>(Arrays.asList(
+ TAG_F_NUMBER, TAG_EXPOSURE_TIME, TAG_GPS_TIMESTAMP));
+
+ private static final int MM_IN_MICRONS = 1000;
+
+ private final List<Map<String, ExifAttribute>> mAttributes;
+ private final ByteOrder mByteOrder;
+
+ ExifData(ByteOrder order, List<Map<String, ExifAttribute>> attributes) {
+ Preconditions.checkState(attributes.size() == EXIF_TAGS.length, "Malformed attributes "
+ + "list. Number of IFDs mismatch.");
+ mByteOrder = order;
+ mAttributes = attributes;
+ }
+
+ /**
+ * Gets the byte order.
+ */
+ @NonNull
+ public ByteOrder getByteOrder() {
+ return mByteOrder;
+ }
+
+ @NonNull
+ Map<String, ExifAttribute> getAttributes(int ifdIndex) {
+ Preconditions.checkArgumentInRange(ifdIndex, 0, EXIF_TAGS.length,
+ "Invalid IFD index: " + ifdIndex + ". Index should be between [0, EXIF_TAGS"
+ + ".length] ");
+ return mAttributes.get(ifdIndex);
+ }
+
+ /**
+ * Returns the value of the specified tag or {@code null} if there
+ * is no such tag in the image file.
+ *
+ * @param tag the name of the tag.
+ */
+ @Nullable
+ public String getAttribute(@NonNull String tag) {
+ ExifAttribute attribute = getExifAttribute(tag);
+ if (attribute != null) {
+ if (!sTagSetForCompatibility.contains(tag)) {
+ return attribute.getStringValue(mByteOrder);
+ }
+ if (tag.equals(TAG_GPS_TIMESTAMP)) {
+ // Convert the rational values to the custom formats for backwards compatibility.
+ if (attribute.format != IFD_FORMAT_URATIONAL
+ && attribute.format != IFD_FORMAT_SRATIONAL) {
+ Logger.w(TAG,
+ "GPS Timestamp format is not rational. format=" + attribute.format);
+ return null;
+ }
+ LongRational[] array =
+ (LongRational[]) attribute.getValue(mByteOrder);
+ if (array == null || array.length != 3) {
+ Logger.w(TAG, "Invalid GPS Timestamp array. array=" + Arrays.toString(array));
+ return null;
+ }
+ return String.format(Locale.US, "%02d:%02d:%02d",
+ (int) ((float) array[0].getNumerator() / array[0].getDenominator()),
+ (int) ((float) array[1].getNumerator() / array[1].getDenominator()),
+ (int) ((float) array[2].getNumerator() / array[2].getDenominator()));
+ }
+ try {
+ return Double.toString(attribute.getDoubleValue(mByteOrder));
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the EXIF attribute of the specified tag or {@code null} if there is no such tag.
+ *
+ * @param tag the name of the tag.
+ */
+ @SuppressWarnings("deprecation")
+ @Nullable
+ private ExifAttribute getExifAttribute(@NonNull String tag) {
+ // Maintain compatibility.
+ if (TAG_ISO_SPEED_RATINGS.equals(tag)) {
+ if (DEBUG) {
+ Logger.d(TAG, "getExifAttribute: Replacing TAG_ISO_SPEED_RATINGS with "
+ + "TAG_PHOTOGRAPHIC_SENSITIVITY.");
+ }
+ tag = TAG_PHOTOGRAPHIC_SENSITIVITY;
+ }
+ // Retrieves all tag groups. The value from primary image tag group has a higher priority
+ // than the value from the thumbnail tag group if there are more than one candidates.
+ for (int i = 0; i < EXIF_TAGS.length; ++i) {
+ ExifAttribute value = mAttributes.get(i).get(tag);
+ if (value != null) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Generates an empty builder suitable for generating ExifData for JPEG from the current device.
+ */
+ @NonNull
+ public static Builder builderForDevice() {
+ // Add PRIMARY defaults. EXIF and GPS defaults will be added in build()
+ return new Builder(ByteOrder.BIG_ENDIAN)
+ .setAttribute(TAG_ORIENTATION, String.valueOf(ORIENTATION_NORMAL))
+ .setAttribute(TAG_X_RESOLUTION, "72/1")
+ .setAttribute(TAG_Y_RESOLUTION, "72/1")
+ .setAttribute(TAG_RESOLUTION_UNIT, String.valueOf(RESOLUTION_UNIT_INCHES))
+ .setAttribute(TAG_Y_CB_CR_POSITIONING,
+ String.valueOf(Y_CB_CR_POSITIONING_CENTERED))
+ // Defaults derived from device
+ .setAttribute(TAG_MAKE, Build.MANUFACTURER)
+ .setAttribute(TAG_MODEL, Build.MODEL);
+ }
+
+ /**
+ * Builder for the {@link ExifData} class.
+ */
+ public static final class Builder {
+ // Pattern to check gps timestamp
+ private static final Pattern GPS_TIMESTAMP_PATTERN =
+ Pattern.compile("^(\\d{2}):(\\d{2}):(\\d{2})$");
+ // Pattern to check date time primary format (e.g. 2020:01:01 00:00:00)
+ private static final Pattern DATETIME_PRIMARY_FORMAT_PATTERN =
+ Pattern.compile("^(\\d{4}):(\\d{2}):(\\d{2})\\s(\\d{2}):(\\d{2}):(\\d{2})$");
+ // Pattern to check date time secondary format (e.g. 2020-01-01 00:00:00)
+ private static final Pattern DATETIME_SECONDARY_FORMAT_PATTERN =
+ Pattern.compile("^(\\d{4})-(\\d{2})-(\\d{2})\\s(\\d{2}):(\\d{2}):(\\d{2})$");
+ private static final int DATETIME_VALUE_STRING_LENGTH = 19;
+
+ // Mappings from tag name to tag number and each item represents one IFD tag group.
+ static final List<HashMap<String, ExifTag>> sExifTagMapsForWriting =
+ Collections.list(new Enumeration<HashMap<String, ExifTag>>() {
+ int mIfdIndex = 0;
+
+ @Override
+ public boolean hasMoreElements() {
+ return mIfdIndex < EXIF_TAGS.length;
+ }
+
+ @Override
+ public HashMap<String, ExifTag> nextElement() {
+ // Build up the hash tables to look up Exif tags for writing Exif tags.
+ HashMap<String, ExifTag> map = new HashMap<>();
+ for (ExifTag tag : EXIF_TAGS[mIfdIndex]) {
+ map.put(tag.name, tag);
+ }
+ mIfdIndex++;
+ return map;
+ }
+ });
+
+ final List<Map<String, ExifAttribute>> mAttributes = Collections.list(
+ new Enumeration<Map<String, ExifAttribute>>() {
+ int mIfdIndex = 0;
+
+ @Override
+ public boolean hasMoreElements() {
+ return mIfdIndex < EXIF_TAGS.length;
+ }
+
+ @Override
+ public Map<String, ExifAttribute> nextElement() {
+ mIfdIndex++;
+ return new HashMap<>();
+ }
+ });
+ private final ByteOrder mByteOrder;
+
+ Builder(@NonNull ByteOrder byteOrder) {
+ mByteOrder = byteOrder;
+ }
+
+ /**
+ * Sets the width of the image.
+ *
+ * @param width the width of the image.
+ */
+ @NonNull
+ public Builder setImageWidth(int width) {
+ return setAttribute(TAG_IMAGE_WIDTH, String.valueOf(width));
+ }
+
+ /**
+ * Sets the height of the image.
+ *
+ * @param height the height of the image.
+ */
+ @NonNull
+ public Builder setImageHeight(int height) {
+ return setAttribute(TAG_IMAGE_LENGTH, String.valueOf(height));
+ }
+
+ /**
+ * Sets the orientation of the image in degrees.
+ *
+ * @param orientationDegrees the orientation in degrees. Can be one of (0, 90, 180, 270)
+ */
+ @NonNull
+ public Builder setOrientationDegrees(int orientationDegrees) {
+ int orientationEnum;
+ switch (orientationDegrees) {
+ case 0:
+ orientationEnum = ExifInterface.ORIENTATION_NORMAL;
+ break;
+ case 90:
+ orientationEnum = ExifInterface.ORIENTATION_ROTATE_90;
+ break;
+ case 180:
+ orientationEnum = ExifInterface.ORIENTATION_ROTATE_180;
+ break;
+ case 270:
+ orientationEnum = ExifInterface.ORIENTATION_ROTATE_270;
+ break;
+ default:
+ Logger.w(TAG,
+ "Unexpected orientation value: " + orientationDegrees
+ + ". Must be one of 0, 90, 180, 270.");
+ orientationEnum = ExifInterface.ORIENTATION_UNDEFINED;
+ break;
+ }
+ return setAttribute(TAG_ORIENTATION, String.valueOf(orientationEnum));
+ }
+
+ /**
+ * Sets the flash information from
+ * {@link androidx.camera.core.impl.CameraCaptureMetaData.FlashState}.
+ *
+ * @param flashState the state of the flash at capture time.
+ */
+ @NonNull
+ public Builder setFlashState(@NonNull CameraCaptureMetaData.FlashState flashState) {
+ if (flashState == CameraCaptureMetaData.FlashState.UNKNOWN) {
+ // Cannot set flash state information
+ return this;
+ }
+
+ short value;
+ switch (flashState) {
+ case READY:
+ value = 0;
+ break;
+ case NONE:
+ value = FLAG_FLASH_NO_FLASH_FUNCTION;
+ break;
+ case FIRED:
+ value = FLAG_FLASH_FIRED;
+ break;
+ default:
+ Logger.w(TAG, "Unknown flash state: " + flashState);
+ return this;
+ }
+
+ if ((value & FLAG_FLASH_FIRED) == FLAG_FLASH_FIRED) {
+ // Set light source to flash
+ setAttribute(TAG_LIGHT_SOURCE, String.valueOf(LIGHT_SOURCE_FLASH));
+ }
+
+
+ return setAttribute(TAG_FLASH, String.valueOf(value));
+ }
+
+ /**
+ * Sets the amount of time the sensor was exposed for, in nanoseconds.
+ * @param exposureTimeNs The exposure time in nanoseconds.
+ */
+ @NonNull
+ public Builder setExposureTimeNanos(long exposureTimeNs) {
+ return setAttribute(TAG_EXPOSURE_TIME,
+ String.valueOf(exposureTimeNs / (double) TimeUnit.SECONDS.toNanos(1)));
+ }
+
+ /**
+ * Sets the lens f-number.
+ *
+ * <p>The lens f-number has precision 1.xx, for example, 1.80.
+ * @param fNumber The f-number.
+ */
+ @NonNull
+ public Builder setLensFNumber(float fNumber) {
+ return setAttribute(TAG_F_NUMBER, String.valueOf(fNumber));
+ }
+
+ /**
+ * Sets the ISO.
+ *
+ * @param iso the standard ISO sensitivity value, as defined in ISO 12232:2006.
+ */
+ @NonNull
+ public Builder setIso(int iso) {
+ return setAttribute(TAG_SENSITIVITY_TYPE, String.valueOf(SENSITIVITY_TYPE_ISO_SPEED))
+ .setAttribute(TAG_PHOTOGRAPHIC_SENSITIVITY, String.valueOf(Math.min(65535,
+ iso)));
+ }
+
+ /**
+ * Sets lens focal length, in millimeters.
+ *
+ * @param focalLength The lens focal length in millimeters.
+ */
+ @NonNull
+ public Builder setFocalLength(float focalLength) {
+ LongRational focalLengthRational =
+ new LongRational((long) (focalLength * MM_IN_MICRONS), MM_IN_MICRONS);
+ return setAttribute(TAG_FOCAL_LENGTH, focalLengthRational.toString());
+ }
+
+ /**
+ * Sets the white balance mode.
+ *
+ * @param whiteBalanceMode The white balance mode. One of {@link WhiteBalanceMode#AUTO}
+ * or {@link WhiteBalanceMode#MANUAL}.
+ */
+ @NonNull
+ public Builder setWhiteBalanceMode(@NonNull WhiteBalanceMode whiteBalanceMode) {
+ String wbString = null;
+ switch (whiteBalanceMode) {
+ case AUTO:
+ wbString = String.valueOf(WHITE_BALANCE_AUTO);
+ break;
+ case MANUAL:
+ wbString = String.valueOf(WHITE_BALANCE_MANUAL);
+ break;
+ }
+ return setAttribute(TAG_WHITE_BALANCE, wbString);
+ }
+
+ /**
+ * Sets the value of the specified tag.
+ *
+ * @param tag the name of the tag.
+ * @param value the value of the tag.
+ */
+ @NonNull
+ public Builder setAttribute(@NonNull String tag, @NonNull String value) {
+ setAttributeInternal(tag, value, mAttributes);
+ return this;
+ }
+
+ /**
+ * Removes the attribute with the given tag.
+ *
+ * @param tag the name of the tag.
+ */
+ @NonNull
+ public Builder removeAttribute(@NonNull String tag) {
+ setAttributeInternal(tag, null, mAttributes);
+ return this;
+ }
+
+ private void setAttributeIfMissing(@NonNull String tag, @NonNull String value,
+ @NonNull List<Map<String, ExifAttribute>> attributes) {
+ for (Map<String, ExifAttribute> attrs : attributes) {
+ if (attrs.containsKey(tag)) {
+ // Attr already exists
+ return;
+ }
+ }
+
+ // Add missing attribute.
+ setAttributeInternal(tag, value, attributes);
+ }
+
+ @SuppressWarnings("deprecation")
+ // Allows null values to remove attributes
+ private void setAttributeInternal(@NonNull String tag, @Nullable String value,
+ @NonNull List<Map<String, ExifAttribute>> attributes) {
+ // Validate and convert if necessary.
+ if (TAG_DATETIME.equals(tag) || TAG_DATETIME_ORIGINAL.equals(tag)
+ || TAG_DATETIME_DIGITIZED.equals(tag)) {
+ if (value != null) {
+ boolean isPrimaryFormat = DATETIME_PRIMARY_FORMAT_PATTERN.matcher(value).find();
+ boolean isSecondaryFormat = DATETIME_SECONDARY_FORMAT_PATTERN.matcher(
+ value).find();
+ // Validate
+ if (value.length() != DATETIME_VALUE_STRING_LENGTH
+ || (!isPrimaryFormat && !isSecondaryFormat)) {
+ Logger.w(TAG, "Invalid value for " + tag + " : " + value);
+ return;
+ }
+ // If datetime value has secondary format (e.g. 2020-01-01 00:00:00), convert it
+ // to primary format (e.g. 2020:01:01 00:00:00) since it is the format in the
+ // official documentation.
+ // See JEITA CP-3451C Section 4.6.4. D. Other Tags, DateTime
+ if (isSecondaryFormat) {
+ // Replace "-" with ":" to match the primary format.
+ value = value.replaceAll("-", ":");
+ }
+ }
+ }
+ // Maintain compatibility.
+ if (TAG_ISO_SPEED_RATINGS.equals(tag)) {
+ if (DEBUG) {
+ Logger.d(TAG, "setAttribute: Replacing TAG_ISO_SPEED_RATINGS with "
+ + "TAG_PHOTOGRAPHIC_SENSITIVITY.");
+ }
+ tag = TAG_PHOTOGRAPHIC_SENSITIVITY;
+ }
+ // Convert the given value to rational values for backwards compatibility.
+ if (value != null && sTagSetForCompatibility.contains(tag)) {
+ if (tag.equals(TAG_GPS_TIMESTAMP)) {
+ Matcher m = GPS_TIMESTAMP_PATTERN.matcher(value);
+ if (!m.find()) {
+ Logger.w(TAG, "Invalid value for " + tag + " : " + value);
+ return;
+ }
+ value = Integer.parseInt(Preconditions.checkNotNull(m.group(1))) + "/1,"
+ + Integer.parseInt(Preconditions.checkNotNull(m.group(2))) + "/1,"
+ + Integer.parseInt(Preconditions.checkNotNull(m.group(3))) + "/1";
+ } else {
+ try {
+ double doubleValue = Double.parseDouble(value);
+ value = new LongRational(doubleValue).toString();
+ } catch (NumberFormatException e) {
+ Logger.w(TAG, "Invalid value for " + tag + " : " + value, e);
+ return;
+ }
+ }
+ }
+
+ for (int i = 0; i < EXIF_TAGS.length; ++i) {
+ final ExifTag exifTag = sExifTagMapsForWriting.get(i).get(tag);
+ if (exifTag != null) {
+ if (value == null) {
+ attributes.get(i).remove(tag);
+ continue;
+ }
+ Pair<Integer, Integer> guess = guessDataFormat(value);
+ int dataFormat;
+ if (exifTag.primaryFormat == guess.first
+ || exifTag.primaryFormat == guess.second) {
+ dataFormat = exifTag.primaryFormat;
+ } else if (exifTag.secondaryFormat != -1 && (
+ exifTag.secondaryFormat == guess.first
+ || exifTag.secondaryFormat == guess.second)) {
+ dataFormat = exifTag.secondaryFormat;
+ } else if (exifTag.primaryFormat == IFD_FORMAT_BYTE
+ || exifTag.primaryFormat == IFD_FORMAT_UNDEFINED
+ || exifTag.primaryFormat == IFD_FORMAT_STRING) {
+ dataFormat = exifTag.primaryFormat;
+ } else {
+ if (DEBUG) {
+ Logger.d(TAG, "Given tag (" + tag
+ + ") value didn't match with one of expected "
+ + "formats: " + IFD_FORMAT_NAMES[exifTag.primaryFormat]
+ + (exifTag.secondaryFormat == -1 ? "" : ", "
+ + IFD_FORMAT_NAMES[exifTag.secondaryFormat]) + " (guess: "
+ + IFD_FORMAT_NAMES[guess.first] + (guess.second == -1 ? ""
+ : ", "
+ + IFD_FORMAT_NAMES[guess.second]) + ")");
+ }
+ continue;
+ }
+ switch (dataFormat) {
+ case IFD_FORMAT_BYTE: {
+ attributes.get(i).put(tag, ExifAttribute.createByte(value));
+ break;
+ }
+ case IFD_FORMAT_UNDEFINED:
+ case IFD_FORMAT_STRING: {
+ attributes.get(i).put(tag, ExifAttribute.createString(value));
+ break;
+ }
+ case IFD_FORMAT_USHORT: {
+ final String[] values = value.split(",", -1);
+ final int[] intArray = new int[values.length];
+ for (int j = 0; j < values.length; ++j) {
+ intArray[j] = Integer.parseInt(values[j]);
+ }
+ attributes.get(i).put(tag,
+ ExifAttribute.createUShort(intArray, mByteOrder));
+ break;
+ }
+ case IFD_FORMAT_SLONG: {
+ final String[] values = value.split(",", -1);
+ final int[] intArray = new int[values.length];
+ for (int j = 0; j < values.length; ++j) {
+ intArray[j] = Integer.parseInt(values[j]);
+ }
+ attributes.get(i).put(tag,
+ ExifAttribute.createSLong(intArray, mByteOrder));
+ break;
+ }
+ case IFD_FORMAT_ULONG: {
+ final String[] values = value.split(",", -1);
+ final long[] longArray = new long[values.length];
+ for (int j = 0; j < values.length; ++j) {
+ longArray[j] = Long.parseLong(values[j]);
+ }
+ attributes.get(i).put(tag,
+ ExifAttribute.createULong(longArray, mByteOrder));
+ break;
+ }
+ case IFD_FORMAT_URATIONAL: {
+ final String[] values = value.split(",", -1);
+ final LongRational[] rationalArray = new LongRational[values.length];
+ for (int j = 0; j < values.length; ++j) {
+ final String[] numbers = values[j].split("/", -1);
+ rationalArray[j] = new LongRational(
+ (long) Double.parseDouble(numbers[0]),
+ (long) Double.parseDouble(numbers[1]));
+ }
+ attributes.get(i).put(tag,
+ ExifAttribute.createURational(rationalArray, mByteOrder));
+ break;
+ }
+ case IFD_FORMAT_SRATIONAL: {
+ final String[] values = value.split(",", -1);
+ final LongRational[] rationalArray = new LongRational[values.length];
+ for (int j = 0; j < values.length; ++j) {
+ final String[] numbers = values[j].split("/", -1);
+ rationalArray[j] = new LongRational(
+ (long) Double.parseDouble(numbers[0]),
+ (long) Double.parseDouble(numbers[1]));
+ }
+ attributes.get(i).put(tag,
+ ExifAttribute.createSRational(rationalArray, mByteOrder));
+ break;
+ }
+ case IFD_FORMAT_DOUBLE: {
+ final String[] values = value.split(",", -1);
+ final double[] doubleArray = new double[values.length];
+ for (int j = 0; j < values.length; ++j) {
+ doubleArray[j] = Double.parseDouble(values[j]);
+ }
+ attributes.get(i).put(tag,
+ ExifAttribute.createDouble(doubleArray, mByteOrder));
+ break;
+ }
+ default:
+ if (DEBUG) {
+ Logger.d(TAG,
+ "Data format isn't one of expected formats: " + dataFormat);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Builds an {@link ExifData} from the current state of the builder.
+ */
+ @NonNull
+ public ExifData build() {
+ // Create a read-only copy of all attributes. This needs to be a deep copy since
+ // build() can be called multiple times. We'll remove null values as well.
+ List<Map<String, ExifAttribute>> attributes = Collections.list(
+ new Enumeration<Map<String, ExifAttribute>>() {
+ final Enumeration<Map<String, ExifAttribute>> mMapEnumeration =
+ Collections.enumeration(mAttributes);
+
+ @Override
+ public boolean hasMoreElements() {
+ return mMapEnumeration.hasMoreElements();
+ }
+
+ @Override
+ public Map<String, ExifAttribute> nextElement() {
+ return new HashMap<>(mMapEnumeration.nextElement());
+ }
+ });
+ // Add EXIF defaults if needed
+ if (!attributes.get(IFD_TYPE_EXIF).isEmpty()) {
+ setAttributeIfMissing(TAG_EXPOSURE_PROGRAM,
+ String.valueOf(EXPOSURE_PROGRAM_NOT_DEFINED), attributes);
+ setAttributeIfMissing(TAG_EXIF_VERSION, "0230", attributes);
+ // Default is for YCbCr components
+ setAttributeIfMissing(TAG_COMPONENTS_CONFIGURATION, "1,2,3,0", attributes);
+ setAttributeIfMissing(TAG_METERING_MODE, String.valueOf(METERING_MODE_UNKNOWN),
+ attributes);
+ setAttributeIfMissing(TAG_LIGHT_SOURCE, String.valueOf(LIGHT_SOURCE_UNKNOWN),
+ attributes);
+ setAttributeIfMissing(TAG_FLASHPIX_VERSION, "0100", attributes);
+ setAttributeIfMissing(TAG_FOCAL_PLANE_RESOLUTION_UNIT,
+ String.valueOf(RESOLUTION_UNIT_INCHES), attributes);
+ setAttributeIfMissing(TAG_FILE_SOURCE, String.valueOf(FILE_SOURCE_DSC), attributes);
+ setAttributeIfMissing(TAG_SCENE_TYPE,
+ String.valueOf(SCENE_TYPE_DIRECTLY_PHOTOGRAPHED), attributes);
+ setAttributeIfMissing(TAG_CUSTOM_RENDERED, String.valueOf(RENDERED_PROCESS_NORMAL),
+ attributes);
+ setAttributeIfMissing(TAG_SCENE_CAPTURE_TYPE,
+ String.valueOf(SCENE_CAPTURE_TYPE_STANDARD), attributes);
+ setAttributeIfMissing(TAG_CONTRAST, String.valueOf(CONTRAST_NORMAL), attributes);
+ setAttributeIfMissing(TAG_SATURATION, String.valueOf(SATURATION_NORMAL),
+ attributes);
+ setAttributeIfMissing(TAG_SHARPNESS, String.valueOf(SHARPNESS_NORMAL), attributes);
+ }
+ // Add GPS defaults if needed
+ if (!attributes.get(IFD_TYPE_GPS).isEmpty()) {
+ setAttributeIfMissing(TAG_GPS_VERSION_ID, "2300", attributes);
+ setAttributeIfMissing(TAG_GPS_SPEED_REF, GPS_SPEED_KILOMETERS_PER_HOUR, attributes);
+ setAttributeIfMissing(TAG_GPS_TRACK_REF, GPS_DIRECTION_TRUE, attributes);
+ setAttributeIfMissing(TAG_GPS_IMG_DIRECTION_REF, GPS_DIRECTION_TRUE, attributes);
+ setAttributeIfMissing(TAG_GPS_DEST_BEARING_REF, GPS_DIRECTION_TRUE, attributes);
+ setAttributeIfMissing(TAG_GPS_DEST_DISTANCE_REF, GPS_DISTANCE_KILOMETERS,
+ attributes);
+ }
+ return new ExifData(mByteOrder, attributes);
+ }
+
+ /**
+ * Determines the data format of EXIF entry value.
+ *
+ * @param entryValue The value to be determined.
+ * @return Returns two data formats guessed as a pair in integer. If there is no two
+ * candidate
+ * data formats for the given entry value, returns {@code -1} in the second of the pair.
+ */
+ private static Pair<Integer, Integer> guessDataFormat(String entryValue) {
+ // See TIFF 6.0 Section 2, "Image File Directory".
+ // Take the first component if there are more than one component.
+ if (entryValue.contains(",")) {
+ String[] entryValues = entryValue.split(",", -1);
+ Pair<Integer, Integer> dataFormat = guessDataFormat(entryValues[0]);
+ if (dataFormat.first == IFD_FORMAT_STRING) {
+ return dataFormat;
+ }
+ for (int i = 1; i < entryValues.length; ++i) {
+ final Pair<Integer, Integer> guessDataFormat = guessDataFormat(entryValues[i]);
+ int first = -1, second = -1;
+ if (guessDataFormat.first.equals(dataFormat.first)
+ || guessDataFormat.second.equals(dataFormat.first)) {
+ first = dataFormat.first;
+ }
+ if (dataFormat.second != -1 && (guessDataFormat.first.equals(dataFormat.second)
+ || guessDataFormat.second.equals(dataFormat.second))) {
+ second = dataFormat.second;
+ }
+ if (first == -1 && second == -1) {
+ return new Pair<>(IFD_FORMAT_STRING, -1);
+ }
+ if (first == -1) {
+ dataFormat = new Pair<>(second, -1);
+ continue;
+ }
+ if (second == -1) {
+ dataFormat = new Pair<>(first, -1);
+ }
+ }
+ return dataFormat;
+ }
+
+ if (entryValue.contains("/")) {
+ String[] rationalNumber = entryValue.split("/", -1);
+ if (rationalNumber.length == 2) {
+ try {
+ long numerator = (long) Double.parseDouble(rationalNumber[0]);
+ long denominator = (long) Double.parseDouble(rationalNumber[1]);
+ if (numerator < 0L || denominator < 0L) {
+ return new Pair<>(IFD_FORMAT_SRATIONAL, -1);
+ }
+ if (numerator > Integer.MAX_VALUE || denominator > Integer.MAX_VALUE) {
+ return new Pair<>(IFD_FORMAT_URATIONAL, -1);
+ }
+ return new Pair<>(IFD_FORMAT_SRATIONAL, IFD_FORMAT_URATIONAL);
+ } catch (NumberFormatException e) {
+ // Ignored
+ }
+ }
+ return new Pair<>(IFD_FORMAT_STRING, -1);
+ }
+ try {
+ long longValue = Long.parseLong(entryValue);
+ if (longValue >= 0 && longValue <= 65535) {
+ return new Pair<>(IFD_FORMAT_USHORT, IFD_FORMAT_ULONG);
+ }
+ if (longValue < 0) {
+ return new Pair<>(IFD_FORMAT_SLONG, -1);
+ }
+ return new Pair<>(IFD_FORMAT_ULONG, -1);
+ } catch (NumberFormatException e) {
+ // Ignored
+ }
+ try {
+ Double.parseDouble(entryValue);
+ return new Pair<>(IFD_FORMAT_DOUBLE, -1);
+ } catch (NumberFormatException e) {
+ // Ignored
+ }
+ return new Pair<>(IFD_FORMAT_STRING, -1);
+ }
+ }
+}
+
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifOutputStream.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifOutputStream.java
new file mode 100644
index 0000000..a2594fd
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifOutputStream.java
@@ -0,0 +1,387 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl.utils;
+
+import static androidx.camera.core.impl.utils.ExifAttribute.ASCII;
+import static androidx.camera.core.impl.utils.ExifData.Builder.sExifTagMapsForWriting;
+import static androidx.camera.core.impl.utils.ExifData.EXIF_POINTER_TAGS;
+import static androidx.camera.core.impl.utils.ExifData.EXIF_TAGS;
+import static androidx.camera.core.impl.utils.ExifData.IFD_TYPE_EXIF;
+import static androidx.camera.core.impl.utils.ExifData.IFD_TYPE_GPS;
+import static androidx.camera.core.impl.utils.ExifData.IFD_TYPE_INTEROPERABILITY;
+import static androidx.camera.core.impl.utils.ExifData.IFD_TYPE_PRIMARY;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.Logger;
+import androidx.core.util.Preconditions;
+
+import java.io.BufferedOutputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * This class provides a way to replace the Exif header of a JPEG image.
+ * <p>
+ * Below is an example of writing EXIF data into a file
+ *
+ * <pre>
+ * public static void writeExif(byte[] jpeg, ExifData exif, String path) {
+ * OutputStream os = null;
+ * try {
+ * os = new FileOutputStream(path);
+ * // Set the exif header on the output stream
+ * ExifOutputStream eos = new ExifOutputStream(os, exif);
+ * // Write the original jpeg out, the header will be added into the file.
+ * eos.write(jpeg);
+ * } catch (FileNotFoundException e) {
+ * e.printStackTrace();
+ * } catch (IOException e) {
+ * e.printStackTrace();
+ * } finally {
+ * if (os != null) {
+ * try {
+ * os.close();
+ * } catch (IOException e) {
+ * e.printStackTrace();
+ * }
+ * }
+ * }
+ * }
+ * </pre>
+ */
+public final class ExifOutputStream extends FilterOutputStream {
+ private static final String TAG = "ExifOutputStream";
+ private static final boolean DEBUG = false;
+ private static final int STREAMBUFFER_SIZE = 0x00010000; // 64Kb
+
+ private static final int STATE_SOI = 0;
+ private static final int STATE_FRAME_HEADER = 1;
+ private static final int STATE_JPEG_DATA = 2;
+
+ // Identifier for EXIF APP1 segment in JPEG
+ private static final byte[] IDENTIFIER_EXIF_APP1 = "Exif\0\0".getBytes(ASCII);
+
+ // Types of Exif byte alignments (see JEITA CP-3451C Section 4.5.2)
+ private static final short BYTE_ALIGN_II = 0x4949; // II: Intel order
+ private static final short BYTE_ALIGN_MM = 0x4d4d; // MM: Motorola order
+
+ // TIFF Header Fixed Constant (see JEITA CP-3451C Section 4.5.2)
+ private static final byte START_CODE = 0x2a; // 42
+ private static final int IFD_OFFSET = 8;
+
+ private final ExifData mExifData;
+ private final byte[] mSingleByteArray = new byte[1];
+ private final ByteBuffer mBuffer = ByteBuffer.allocate(4);
+ private int mState = STATE_SOI;
+ private int mByteToSkip;
+ private int mByteToCopy;
+
+ /**
+ * Creates an ExifOutputStream that wraps the given {@link OutputStream} and overwrites exif
+ * with the provided {@link ExifData}.
+ * @param ou OutputStream which will be sent the final output.
+ * @param exifData Exif data which will overwrite any exif data sent to this stream.
+ */
+ public ExifOutputStream(@NonNull OutputStream ou, @NonNull ExifData exifData) {
+ super(new BufferedOutputStream(ou, STREAMBUFFER_SIZE));
+ mExifData = exifData;
+ }
+
+ private int requestByteToBuffer(int requestByteCount, byte[] buffer, int offset, int length) {
+ int byteNeeded = requestByteCount - mBuffer.position();
+ int byteToRead = Math.min(length, byteNeeded);
+ mBuffer.put(buffer, offset, byteToRead);
+ return byteToRead;
+ }
+
+ /**
+ * Writes the image out. The input data should be a valid JPEG format. After
+ * writing, it's Exif header will be replaced by the given header.
+ */
+ @Override
+ public void write(@NonNull byte[] buffer, int offset, int length) throws IOException {
+ while ((mByteToSkip > 0 || mByteToCopy > 0 || mState != STATE_JPEG_DATA)
+ && length > 0) {
+ if (mByteToSkip > 0) {
+ int byteToProcess = Math.min(length, mByteToSkip);
+ length -= byteToProcess;
+ mByteToSkip -= byteToProcess;
+ offset += byteToProcess;
+ }
+ if (mByteToCopy > 0) {
+ int byteToProcess = Math.min(length, mByteToCopy);
+ out.write(buffer, offset, byteToProcess);
+ length -= byteToProcess;
+ mByteToCopy -= byteToProcess;
+ offset += byteToProcess;
+ }
+ if (length == 0) {
+ return;
+ }
+ switch (mState) {
+ case STATE_SOI:
+ int byteRead = requestByteToBuffer(2, buffer, offset, length);
+ offset += byteRead;
+ length -= byteRead;
+ if (mBuffer.position() < 2) {
+ return;
+ }
+ mBuffer.rewind();
+ if (mBuffer.getShort() != JpegHeader.SOI) {
+ throw new IOException("Not a valid jpeg image, cannot write exif");
+ }
+ out.write(mBuffer.array(), 0, 2);
+ mState = STATE_FRAME_HEADER;
+ mBuffer.rewind();
+ ByteOrderedDataOutputStream dataOutputStream =
+ new ByteOrderedDataOutputStream(out, ByteOrder.BIG_ENDIAN);
+ dataOutputStream.writeShort(JpegHeader.APP1);
+ writeExifSegment(dataOutputStream);
+ break;
+ case STATE_FRAME_HEADER:
+ // We ignore the APP1 segment and copy all other segments
+ // until SOF tag.
+ byteRead = requestByteToBuffer(4, buffer, offset, length);
+ offset += byteRead;
+ length -= byteRead;
+ // Check if this image data doesn't contain SOF.
+ if (mBuffer.position() == 2) {
+ short tag = mBuffer.getShort();
+ if (tag == JpegHeader.EOI) {
+ out.write(mBuffer.array(), 0, 2);
+ mBuffer.rewind();
+ }
+ }
+ if (mBuffer.position() < 4) {
+ return;
+ }
+ mBuffer.rewind();
+ short marker = mBuffer.getShort();
+ if (marker == JpegHeader.APP1) {
+ mByteToSkip = (mBuffer.getShort() & 0x0000ffff) - 2;
+ mState = STATE_JPEG_DATA;
+ } else if (!JpegHeader.isSofMarker(marker)) {
+ out.write(mBuffer.array(), 0, 4);
+ mByteToCopy = (mBuffer.getShort() & 0x0000ffff) - 2;
+ } else {
+ out.write(mBuffer.array(), 0, 4);
+ mState = STATE_JPEG_DATA;
+ }
+ mBuffer.rewind();
+ }
+ }
+ if (length > 0) {
+ out.write(buffer, offset, length);
+ }
+ }
+
+ /**
+ * Writes the one bytes out. The input data should be a valid JPEG format.
+ * After writing, it's Exif header will be replaced by the given header.
+ */
+ @Override
+ public void write(int oneByte) throws IOException {
+ mSingleByteArray[0] = (byte) (0xff & oneByte);
+ write(mSingleByteArray);
+ }
+
+ /**
+ * Equivalent to calling write(buffer, 0, buffer.length).
+ */
+ @Override
+ public void write(@NonNull byte[] buffer) throws IOException {
+ write(buffer, 0, buffer.length);
+ }
+
+ // Writes an Exif segment into the given output stream.
+ private void writeExifSegment(@NonNull ByteOrderedDataOutputStream dataOutputStream)
+ throws IOException {
+ // The following variables are for calculating each IFD tag group size in bytes.
+ int[] ifdOffsets = new int[EXIF_TAGS.length];
+ int[] ifdDataSizes = new int[EXIF_TAGS.length];
+
+ // Remove IFD pointer tags (we'll re-add it later.)
+ for (ExifTag tag : EXIF_POINTER_TAGS) {
+ for (int ifdIndex = 0; ifdIndex < EXIF_TAGS.length; ++ifdIndex) {
+ mExifData.getAttributes(ifdIndex).remove(tag.name);
+ }
+ }
+
+ // Add IFD pointer tags. The next offset of primary image TIFF IFD will have thumbnail IFD
+ // offset when there is one or more tags in the thumbnail IFD.
+ if (!mExifData.getAttributes(IFD_TYPE_EXIF).isEmpty()) {
+ mExifData.getAttributes(IFD_TYPE_PRIMARY).put(EXIF_POINTER_TAGS[1].name,
+ ExifAttribute.createULong(0, mExifData.getByteOrder()));
+ }
+ if (!mExifData.getAttributes(IFD_TYPE_GPS).isEmpty()) {
+ mExifData.getAttributes(IFD_TYPE_PRIMARY).put(EXIF_POINTER_TAGS[2].name,
+ ExifAttribute.createULong(0, mExifData.getByteOrder()));
+ }
+ if (!mExifData.getAttributes(IFD_TYPE_INTEROPERABILITY).isEmpty()) {
+ mExifData.getAttributes(IFD_TYPE_EXIF).put(EXIF_POINTER_TAGS[3].name,
+ ExifAttribute.createULong(0, mExifData.getByteOrder()));
+ }
+
+ // Calculate IFD group data area sizes. IFD group data area is assigned to save the entry
+ // value which has a bigger size than 4 bytes.
+ for (int i = 0; i < EXIF_TAGS.length; ++i) {
+ int sum = 0;
+ for (Map.Entry<String, ExifAttribute> entry : mExifData.getAttributes(i).entrySet()) {
+ final ExifAttribute exifAttribute = entry.getValue();
+ final int size = exifAttribute.size();
+ if (size > 4) {
+ sum += size;
+ }
+ }
+ ifdDataSizes[i] += sum;
+ }
+
+ // Calculate IFD offsets.
+ // 8 bytes are for TIFF headers: 2 bytes (byte order) + 2 bytes (identifier) + 4 bytes
+ // (offset of IFDs)
+ int position = 8;
+ for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
+ if (!mExifData.getAttributes(ifdType).isEmpty()) {
+ ifdOffsets[ifdType] = position;
+ position += 2 + mExifData.getAttributes(ifdType).size() * 12 + 4
+ + ifdDataSizes[ifdType];
+ }
+ }
+
+ int totalSize = position;
+ // Add 8 bytes for APP1 size and identifier data
+ totalSize += 8;
+ if (DEBUG) {
+ for (int i = 0; i < EXIF_TAGS.length; ++i) {
+ Logger.d(TAG, String.format(Locale.US, "index: %d, offsets: %d, tag count: %d, "
+ + "data sizes: %d, total size: %d", i, ifdOffsets[i],
+ mExifData.getAttributes(i).size(),
+ ifdDataSizes[i], totalSize));
+ }
+ }
+
+ // Update IFD pointer tags with the calculated offsets.
+ if (!mExifData.getAttributes(IFD_TYPE_EXIF).isEmpty()) {
+ mExifData.getAttributes(IFD_TYPE_PRIMARY).put(EXIF_POINTER_TAGS[1].name,
+ ExifAttribute.createULong(ifdOffsets[IFD_TYPE_EXIF], mExifData.getByteOrder()));
+ }
+ if (!mExifData.getAttributes(IFD_TYPE_GPS).isEmpty()) {
+ mExifData.getAttributes(IFD_TYPE_PRIMARY).put(EXIF_POINTER_TAGS[2].name,
+ ExifAttribute.createULong(ifdOffsets[IFD_TYPE_GPS], mExifData.getByteOrder()));
+ }
+ if (!mExifData.getAttributes(IFD_TYPE_INTEROPERABILITY).isEmpty()) {
+ mExifData.getAttributes(IFD_TYPE_EXIF).put(EXIF_POINTER_TAGS[3].name,
+ ExifAttribute.createULong(
+ ifdOffsets[IFD_TYPE_INTEROPERABILITY], mExifData.getByteOrder()));
+ }
+
+ // Write JPEG specific data (APP1 size, APP1 identifier)
+ dataOutputStream.writeUnsignedShort(totalSize);
+ dataOutputStream.write(IDENTIFIER_EXIF_APP1);
+
+ // Write TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1.
+ dataOutputStream.writeShort(mExifData.getByteOrder() == ByteOrder.BIG_ENDIAN
+ ? BYTE_ALIGN_MM : BYTE_ALIGN_II);
+ dataOutputStream.setByteOrder(mExifData.getByteOrder());
+ dataOutputStream.writeUnsignedShort(START_CODE);
+ dataOutputStream.writeUnsignedInt(IFD_OFFSET);
+
+ // Write IFD groups. See JEITA CP-3451C Section 4.5.8. Figure 9.
+ for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
+ if (!mExifData.getAttributes(ifdType).isEmpty()) {
+ // See JEITA CP-3451C Section 4.6.2: IFD structure.
+ // Write entry count
+ dataOutputStream.writeUnsignedShort(mExifData.getAttributes(ifdType).size());
+
+ // Write entry info
+ int dataOffset = ifdOffsets[ifdType] + 2 + mExifData.getAttributes(ifdType).size()
+ * 12 + 4;
+ for (Map.Entry<String, ExifAttribute> entry : mExifData.getAttributes(
+ ifdType).entrySet()) {
+ // Convert tag name to tag number.
+ final ExifTag tag = sExifTagMapsForWriting.get(ifdType).get(entry.getKey());
+ final int tagNumber =
+ Preconditions.checkNotNull(tag,
+ "Tag not supported: " + entry.getKey() + ". Tag needs to be "
+ + "ported from ExifInterface to ExifData.").number;
+ final ExifAttribute attribute = entry.getValue();
+ final int size = attribute.size();
+
+ dataOutputStream.writeUnsignedShort(tagNumber);
+ dataOutputStream.writeUnsignedShort(attribute.format);
+ dataOutputStream.writeInt(attribute.numberOfComponents);
+ if (size > 4) {
+ dataOutputStream.writeUnsignedInt(dataOffset);
+ dataOffset += size;
+ } else {
+ dataOutputStream.write(attribute.bytes);
+ // Fill zero up to 4 bytes
+ if (size < 4) {
+ for (int i = size; i < 4; ++i) {
+ dataOutputStream.writeByte(0);
+ }
+ }
+ }
+ }
+
+ // Write the next offset. Since we aren't handling thumbnails, this is just 0.
+ dataOutputStream.writeUnsignedInt(0);
+
+ // Write values of data field exceeding 4 bytes after the next offset.
+ for (Map.Entry<String, ExifAttribute> entry : mExifData.getAttributes(
+ ifdType).entrySet()) {
+ ExifAttribute attribute = entry.getValue();
+
+ if (attribute.bytes.length > 4) {
+ dataOutputStream.write(attribute.bytes, 0, attribute.bytes.length);
+ }
+ }
+ }
+ }
+
+ // Reset the byte order to big endian in order to write remaining parts of the JPEG file.
+ dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
+ }
+
+ static final class JpegHeader {
+ public static final short SOI = (short) 0xFFD8;
+ public static final short APP1 = (short) 0xFFE1;
+ public static final short EOI = (short) 0xFFD9;
+
+ /**
+ * SOF (start of frame). All value between SOF0 and SOF15 is SOF marker except for DHT,
+ * JPG, and DAC marker.
+ */
+ public static final short SOF0 = (short) 0xFFC0;
+ public static final short SOF15 = (short) 0xFFCF;
+ public static final short DHT = (short) 0xFFC4;
+ public static final short JPG = (short) 0xFFC8;
+ public static final short DAC = (short) 0xFFCC;
+
+ public static boolean isSofMarker(short marker) {
+ return marker >= SOF0 && marker <= SOF15 && marker != DHT && marker != JPG
+ && marker != DAC;
+ }
+
+ private JpegHeader() {}
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifTag.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifTag.java
new file mode 100644
index 0000000..c0df33b
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/ExifTag.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl.utils;
+
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_DOUBLE;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_SINGLE;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_SLONG;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_SSHORT;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_ULONG;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_UNDEFINED;
+import static androidx.camera.core.impl.utils.ExifAttribute.IFD_FORMAT_USHORT;
+
+import androidx.exifinterface.media.ExifInterface;
+
+/**
+ * This class stores information of an EXIF tag. For more information about
+ * defined EXIF tags, please read the Jeita EXIF 2.2 standard.
+ *
+ * This class was pulled from the {@link ExifInterface} class.
+ *
+ * @see ExifInterface
+ */
+class ExifTag {
+ public final int number;
+ public final String name;
+ public final int primaryFormat;
+ public final int secondaryFormat;
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ ExifTag(String name, int number, int format) {
+ this.name = name;
+ this.number = number;
+ this.primaryFormat = format;
+ this.secondaryFormat = -1;
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ ExifTag(String name, int number, int primaryFormat, int secondaryFormat) {
+ this.name = name;
+ this.number = number;
+ this.primaryFormat = primaryFormat;
+ this.secondaryFormat = secondaryFormat;
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ boolean isFormatCompatible(int format) {
+ if (primaryFormat == IFD_FORMAT_UNDEFINED || format == IFD_FORMAT_UNDEFINED) {
+ return true;
+ } else if (primaryFormat == format || secondaryFormat == format) {
+ return true;
+ } else if ((primaryFormat == IFD_FORMAT_ULONG || secondaryFormat == IFD_FORMAT_ULONG)
+ && format == IFD_FORMAT_USHORT) {
+ return true;
+ } else if ((primaryFormat == IFD_FORMAT_SLONG || secondaryFormat == IFD_FORMAT_SLONG)
+ && format == IFD_FORMAT_SSHORT) {
+ return true;
+ } else return (primaryFormat == IFD_FORMAT_DOUBLE || secondaryFormat == IFD_FORMAT_DOUBLE)
+ && format == IFD_FORMAT_SINGLE;
+ }
+}
+
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/LongRational.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/LongRational.java
new file mode 100644
index 0000000..3b6353d
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/LongRational.java
@@ -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.camera.core.impl.utils;
+
+import androidx.annotation.NonNull;
+
+/**
+ * The rational data type of EXIF tag. Contains a pair of longs representing the
+ * numerator and denominator of a Rational number.
+ */
+final class LongRational {
+
+ private final long mNumerator;
+ private final long mDenominator;
+
+ /**
+ * Create a Rational with a given numerator and denominator.
+ */
+ LongRational(long nominator, long denominator) {
+ mNumerator = nominator;
+ mDenominator = denominator;
+ }
+
+ /**
+ * Creates a Rational from a double.
+ */
+ LongRational(double value) {
+ this((long) (value * 10000), 10000);
+ }
+
+ /**
+ * Gets the numerator of the rational.
+ */
+ long getNumerator() {
+ return mNumerator;
+ }
+
+ /**
+ * Gets the denominator of the rational
+ */
+ long getDenominator() {
+ return mDenominator;
+ }
+
+ /**
+ * Gets the rational value as type double. Will cause a divide-by-zero error
+ * if the denominator is 0.
+ */
+ double toDouble() {
+ return mNumerator / (double) mDenominator;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return mNumerator + "/" + mDenominator;
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraCaptureResultImageInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraCaptureResultImageInfo.java
index e951da0..ea4f5a5 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraCaptureResultImageInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraCaptureResultImageInfo.java
@@ -20,6 +20,7 @@
import androidx.camera.core.ImageInfo;
import androidx.camera.core.impl.CameraCaptureResult;
import androidx.camera.core.impl.TagBundle;
+import androidx.camera.core.impl.utils.ExifData;
/** An ImageInfo that is created by a {@link CameraCaptureResult}. */
public final class CameraCaptureResultImageInfo implements ImageInfo {
@@ -46,6 +47,11 @@
return 0;
}
+ @Override
+ public void populateExifData(@NonNull ExifData.Builder exifBuilder) {
+ mCameraCaptureResult.populateExifData(exifBuilder);
+ }
+
@NonNull
public CameraCaptureResult getCameraCaptureResult() {
return mCameraCaptureResult;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
index 8d0754a..2c16a41 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
@@ -344,7 +344,8 @@
ConfigPair configPair = configPairMap.get(useCase);
// Combine with default configuration.
UseCaseConfig<?> combinedUseCaseConfig =
- useCase.mergeConfigs(configPair.mExtendedConfig, configPair.mCameraConfig);
+ useCase.mergeConfigs(cameraInfoInternal, configPair.mExtendedConfig,
+ configPair.mCameraConfig);
configToUseCaseMap.put(combinedUseCaseConfig, useCase);
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompat.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompat.java
new file mode 100644
index 0000000..773b1ad
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompat.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat;
+
+import android.media.ImageWriter;
+import android.os.Build;
+import android.view.Surface;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+/**
+ * Helper for accessing features of {@link ImageWriter} in a backwards compatible fashion.
+ */
+@RequiresApi(26)
+public final class ImageWriterCompat {
+
+ /**
+ * <p>
+ * Create a new ImageWriter with given number of max Images and format.
+ * </p>
+ * <p>
+ * The {@code maxImages} parameter determines the maximum number of
+ * {@link android.media.Image} objects that can be be dequeued from the
+ * {@code ImageWriter} simultaneously. Requesting more buffers will use up
+ * more memory, so it is important to use only the minimum number necessary.
+ * </p>
+ * <p>
+ * The format specifies the image format of this ImageWriter. The format
+ * from the {@code surface} will be overridden with this format. For example,
+ * if the surface is obtained from a {@link android.graphics.SurfaceTexture}, the default
+ * format may be {@link android.graphics.PixelFormat#RGBA_8888}. If the application creates an
+ * ImageWriter with this surface and {@link android.graphics.ImageFormat#PRIVATE}, this
+ * ImageWriter will be able to operate with {@link android.graphics.ImageFormat#PRIVATE} Images.
+ * </p>
+ * <p>
+ * Note that the consumer end-point may or may not be able to support Images with different
+ * format, for such case, the application should only use this method if the consumer is able
+ * to consume such images.
+ * </p>
+ * <p>
+ * The input Image size depends on the Surface that is provided by
+ * the downstream consumer end-point.
+ * </p>
+ *
+ * @param surface The destination Surface this writer produces Image data
+ * into.
+ * @param maxImages The maximum number of Images the user will want to
+ * access simultaneously for producing Image data. This should be
+ * as small as possible to limit memory use. Once maxImages
+ * Images are dequeued by the user, one of them has to be queued
+ * back before a new Image can be dequeued for access via
+ * {@link ImageWriter#dequeueInputImage()}.
+ * @param format The format of this ImageWriter. It can be any valid format specified by
+ * {@link android.graphics.ImageFormat} or {@link android.graphics.PixelFormat}.
+ *
+ * @return a new ImageWriter instance.
+ */
+ @NonNull
+ public static ImageWriter newInstance(@NonNull Surface surface,
+ @IntRange(from = 1) int maxImages, int format) {
+ if (Build.VERSION.SDK_INT >= 26) {
+ return ImageWriterCompatApi26Impl.newInstance(surface, maxImages, format);
+ } else if (Build.VERSION.SDK_INT >= 29) {
+ return ImageWriterCompatApi29Impl.newInstance(surface, maxImages, format);
+ }
+
+ throw new RuntimeException(
+ "Unable to call newInstance(Surface, int, int) on API " + Build.VERSION.SDK_INT
+ + ". Version 26 or higher required.");
+ }
+
+ // Class should not be instantiated.
+ private ImageWriterCompat() {
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompatApi26Impl.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompatApi26Impl.java
new file mode 100644
index 0000000..5fb2f66
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompatApi26Impl.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat;
+
+import android.media.ImageWriter;
+import android.os.Build;
+import android.util.Log;
+import android.view.Surface;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.core.util.Preconditions;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+@RequiresApi(26)
+final class ImageWriterCompatApi26Impl {
+ private static final String TAG = "ImageWriterCompatApi26";
+
+ private static Method sNewInstanceMethod;
+
+ static {
+ try {
+ sNewInstanceMethod = ImageWriter.class.getMethod("newInstance", Surface.class,
+ int.class, int.class);
+ } catch (NoSuchMethodException e) {
+ Log.i(TAG, "Unable to initialize via reflection.", e);
+ }
+ }
+
+ @NonNull
+ static ImageWriter newInstance(@NonNull Surface surface, @IntRange(from = 1) int maxImages,
+ int format) {
+ Throwable t = null;
+ if (Build.VERSION.SDK_INT >= 26) {
+ try {
+ return (ImageWriter) Preconditions.checkNotNull(
+ sNewInstanceMethod.invoke(null, surface, maxImages, format));
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ t = e;
+ }
+ }
+
+ throw new RuntimeException("Unable to invoke newInstance(Surface, int, int) via "
+ + "reflection.", t);
+ }
+
+ // Class should not be instantiated.
+ private ImageWriterCompatApi26Impl() {
+ }
+}
+
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompatApi29Impl.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompatApi29Impl.java
new file mode 100644
index 0000000..a2ea19d
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/ImageWriterCompatApi29Impl.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat;
+
+import android.media.ImageWriter;
+import android.view.Surface;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+@RequiresApi(29)
+final class ImageWriterCompatApi29Impl {
+
+ @NonNull
+ static ImageWriter newInstance(@NonNull Surface surface, @IntRange(from = 1) int maxImages,
+ int format) {
+ return ImageWriter.newInstance(surface, maxImages, format);
+ }
+
+ // Class should not be instantiated.
+ private ImageWriterCompatApi29Impl() {
+ }
+}
+
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/package-info.java
similarity index 79%
copy from compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
copy to camera/camera-core/src/main/java/androidx/camera/core/internal/compat/package-info.java
index f9cb2fe..439a386 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/package-info.java
@@ -14,7 +14,10 @@
* limitations under the License.
*/
-package androidx.compose.ui.focus
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.camera.core.internal.compat;
-@RequiresOptIn("The Focus API is experimental and is likely to change in the future.")
-annotation class ExperimentalFocus
\ No newline at end of file
+import androidx.annotation.RestrictTo;
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/CameraXConfigTest.java b/camera/camera-core/src/test/java/androidx/camera/core/CameraXConfigTest.java
index d82f3f7..656a101 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/CameraXConfigTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/CameraXConfigTest.java
@@ -96,4 +96,13 @@
final Integer minLoggingLevel = cameraXConfig.getMinimumLoggingLevel();
assertThat(minLoggingLevel).isEqualTo(Logger.DEFAULT_MIN_LOG_LEVEL);
}
+
+ @Test
+ public void canGetAvailableCamerasSelector() {
+ CameraSelector cameraSelector = new CameraSelector.Builder().build();
+ CameraXConfig cameraXConfig = new CameraXConfig.Builder()
+ .setAvailableCamerasLimiter(cameraSelector)
+ .build();
+ assertThat(cameraXConfig.getAvailableCamerasLimiter(null)).isEqualTo(cameraSelector);
+ }
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/QuirksTest.java b/camera/camera-core/src/test/java/androidx/camera/core/impl/QuirksTest.java
index d880ad5..9b780d9 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/QuirksTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/QuirksTest.java
@@ -21,6 +21,7 @@
import org.junit.Test;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
public class QuirksTest {
@@ -41,7 +42,7 @@
}
@Test
- public void returnNullForInexistentQuirk() {
+ public void returnNullForNonexistentQuirk() {
final Quirk1 quirk1 = new Quirk1();
final Quirk2 quirk2 = new Quirk2();
@@ -54,6 +55,52 @@
assertThat(quirks.get(Quirk3.class)).isNull();
}
+ @Test
+ public void containsReturnsTrueForExistentQuirk() {
+ final Quirk1 quirk1 = new Quirk1();
+
+ final List<Quirk> allQuirks = Collections.singletonList(quirk1);
+
+ final Quirks quirks = new Quirks(allQuirks);
+
+ assertThat(quirks.contains(Quirk1.class)).isTrue();
+ }
+
+ @Test
+ public void containsReturnsFalseForNonexistentQuirk() {
+ final Quirk1 quirk1 = new Quirk1();
+
+ final List<Quirk> allQuirks = Collections.singletonList(quirk1);
+
+ final Quirks quirks = new Quirks(allQuirks);
+
+ assertThat(quirks.contains(Quirk2.class)).isFalse();
+ }
+
+ @Test
+ public void containsReturnsTrueForExistentSuperInterfaceQuirk() {
+ final SubIQuirk subIQuirk = new SubIQuirk();
+
+ final List<Quirk> allQuirks = Collections.singletonList(subIQuirk);
+
+ final Quirks quirks = new Quirks(allQuirks);
+
+ assertThat(quirks.contains(SubIQuirk.class)).isTrue();
+ assertThat(quirks.contains(ISuperQuirk.class)).isTrue();
+ }
+
+ @Test
+ public void containsReturnsTrueForExistentSuperClassQuirk() {
+ final SubQuirk subQuirk = new SubQuirk();
+
+ final List<Quirk> allQuirks = Collections.singletonList(subQuirk);
+
+ final Quirks quirks = new Quirks(allQuirks);
+
+ assertThat(quirks.contains(SubQuirk.class)).isTrue();
+ assertThat(quirks.contains(SuperQuirk.class)).isTrue();
+ }
+
static class Quirk1 implements Quirk {
}
@@ -62,4 +109,17 @@
static class Quirk3 implements Quirk {
}
+
+ interface ISuperQuirk extends Quirk {
+ }
+
+ static class SuperQuirk implements Quirk {
+ }
+
+ static class SubQuirk extends SuperQuirk {
+ }
+
+ static class SubIQuirk implements ISuperQuirk {
+ }
+
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/ExifDataTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/ExifDataTest.kt
new file mode 100644
index 0000000..ac6541c
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/ExifDataTest.kt
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl.utils
+
+import android.os.Build
+import androidx.camera.core.impl.CameraCaptureMetaData
+import androidx.exifinterface.media.ExifInterface
+import androidx.exifinterface.media.ExifInterface.FLAG_FLASH_FIRED
+import androidx.exifinterface.media.ExifInterface.FLAG_FLASH_NO_FLASH_FUNCTION
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import java.util.concurrent.TimeUnit
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class ExifDataTest {
+
+ @Test
+ public fun canSetImageWidth() {
+ val exifData = ExifData.builderForDevice().setImageWidth(100).build()
+ assertThat(exifData.getAttribute(ExifInterface.TAG_IMAGE_WIDTH)).isEqualTo("100")
+ }
+
+ @Test
+ public fun canSetImageHeight() {
+ val exifData = ExifData.builderForDevice().setImageHeight(200).build()
+ assertThat(exifData.getAttribute(ExifInterface.TAG_IMAGE_LENGTH)).isEqualTo("200")
+ }
+
+ @Test
+ public fun canSetOrientationDegrees() {
+ val exifData0 = ExifData.builderForDevice().setOrientationDegrees(0).build()
+ val exifData90 = ExifData.builderForDevice().setOrientationDegrees(90).build()
+ val exifData180 = ExifData.builderForDevice().setOrientationDegrees(180).build()
+ val exifData270 = ExifData.builderForDevice().setOrientationDegrees(270).build()
+
+ assertThat(exifData0.getAttribute(ExifInterface.TAG_ORIENTATION))
+ .isEqualTo("${ExifInterface.ORIENTATION_NORMAL}")
+ assertThat(exifData90.getAttribute(ExifInterface.TAG_ORIENTATION))
+ .isEqualTo("${ExifInterface.ORIENTATION_ROTATE_90}")
+ assertThat(exifData180.getAttribute(ExifInterface.TAG_ORIENTATION))
+ .isEqualTo("${ExifInterface.ORIENTATION_ROTATE_180}")
+ assertThat(exifData270.getAttribute(ExifInterface.TAG_ORIENTATION))
+ .isEqualTo("${ExifInterface.ORIENTATION_ROTATE_270}")
+ }
+
+ @Test
+ public fun settingInvalidOrientationIsUndefined() {
+ // Only 0, 90, 180 and 270 are valid orientations. Use an invalid orientation.
+ val exifData = ExifData.builderForDevice().setOrientationDegrees(42).build()
+
+ assertThat(exifData.getAttribute(ExifInterface.TAG_ORIENTATION))
+ .isEqualTo("${ExifInterface.ORIENTATION_UNDEFINED}")
+ }
+
+ @Test
+ public fun canSetFlashState() {
+ val exifDataFired = ExifData.builderForDevice()
+ .setFlashState(CameraCaptureMetaData.FlashState.FIRED)
+ .build()
+ val exifDataReady = ExifData.builderForDevice()
+ .setFlashState(CameraCaptureMetaData.FlashState.READY)
+ .build()
+ val exifDataNone = ExifData.builderForDevice()
+ .setFlashState(CameraCaptureMetaData.FlashState.NONE)
+ .build()
+
+ // Unknown should not set the attribute
+ val exifDataUnknown = ExifData.builderForDevice()
+ .setFlashState(CameraCaptureMetaData.FlashState.UNKNOWN)
+ .build()
+
+ // Flash fired.
+ assertThat(exifDataFired.getAttribute(ExifInterface.TAG_FLASH)?.toShort())
+ .isEqualTo(FLAG_FLASH_FIRED)
+
+ // Has flash but not fired.
+ assertThat(exifDataReady.getAttribute(ExifInterface.TAG_FLASH)?.toShort())
+ .isEqualTo(0)
+
+ // No flash function.
+ assertThat(exifDataNone.getAttribute(ExifInterface.TAG_FLASH)?.toShort())
+ .isEqualTo(FLAG_FLASH_NO_FLASH_FUNCTION)
+
+ assertThat(exifDataUnknown.getAttribute(ExifInterface.TAG_FLASH)).isNull()
+ }
+
+ @Test
+ public fun canSetExposureTime() {
+ val exifData = ExifData.builderForDevice()
+ .setExposureTimeNanos(TimeUnit.SECONDS.toNanos(5))
+ .build()
+ assertThat(exifData.getAttribute(ExifInterface.TAG_EXPOSURE_TIME)?.toFloat()?.toInt())
+ .isEqualTo(5)
+ }
+
+ @Test
+ public fun canSetLensFNumber() {
+ val exifData = ExifData.builderForDevice()
+ .setLensFNumber(1.2f)
+ .build()
+ assertThat(exifData.getAttribute(ExifInterface.TAG_F_NUMBER)).isEqualTo("1.2")
+ }
+
+ @Test
+ public fun canSetIso() {
+ val exifData = ExifData.builderForDevice()
+ .setIso(800)
+ .build()
+ assertThat(exifData.getAttribute(ExifInterface.TAG_SENSITIVITY_TYPE))
+ .isEqualTo("${ExifInterface.SENSITIVITY_TYPE_ISO_SPEED}")
+ assertThat(exifData.getAttribute(ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY))
+ .isEqualTo("800")
+ }
+
+ @Test
+ public fun canSetFocalLength() {
+ val exifData = ExifData.builderForDevice()
+ .setFocalLength(5400f /*millimeters*/)
+ .build()
+ assertThat(
+ exifData.getAttribute(ExifInterface.TAG_FOCAL_LENGTH)
+ ?.split("/")
+ ?.map(String::toLong)
+ ?.reduce { numerator: Long, denominator: Long -> numerator / denominator }
+ ).isEqualTo(5400)
+ }
+
+ @Test
+ public fun canSetWhiteBalanceMode() {
+ val exifDataAuto = ExifData.builderForDevice()
+ .setWhiteBalanceMode(ExifData.WhiteBalanceMode.AUTO)
+ .build()
+ val exifDataManual = ExifData.builderForDevice()
+ .setWhiteBalanceMode(ExifData.WhiteBalanceMode.MANUAL)
+ .build()
+
+ assertThat(exifDataAuto.getAttribute(ExifInterface.TAG_WHITE_BALANCE)?.toShort())
+ .isEqualTo(ExifInterface.WHITE_BALANCE_AUTO)
+ assertThat(exifDataManual.getAttribute(ExifInterface.TAG_WHITE_BALANCE)?.toShort())
+ .isEqualTo(ExifInterface.WHITE_BALANCE_MANUAL)
+ }
+
+ @Test
+ public fun makeAndModelSetByDefaultBuilder() {
+ val exifDataDefault = ExifData.builderForDevice().build()
+
+ assertThat(exifDataDefault.getAttribute(ExifInterface.TAG_MAKE))
+ .isEqualTo(Build.MANUFACTURER)
+ assertThat(exifDataDefault.getAttribute(ExifInterface.TAG_MODEL))
+ .isEqualTo(Build.MODEL)
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/ImageWriterCompatTest.java b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/ImageWriterCompatTest.java
new file mode 100644
index 0000000..e739bda
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/ImageWriterCompatTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.ImageFormat;
+import android.graphics.SurfaceTexture;
+import android.media.ImageWriter;
+import android.os.Build;
+import android.view.Surface;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.O)
+public final class ImageWriterCompatTest {
+
+ private static final int TEST_MAX_IMAGES = 4;
+ private static final int TEST_IMAGE_FORMAT = ImageFormat.YUV_420_888;
+ private Surface mTestSurface;
+ private SurfaceTexture mTestSurfaceTexture;
+
+ @Before
+ public void setUp() {
+ mTestSurfaceTexture = new SurfaceTexture(/* singleBufferMode= */ false);
+ mTestSurface = new Surface(mTestSurfaceTexture);
+ }
+
+ @After
+ public void tearDown() {
+ mTestSurface.release();
+ mTestSurfaceTexture.release();
+ }
+
+ @Test
+ public void canCreateNewInstance() {
+ ImageWriter imageWriter = ImageWriterCompat.newInstance(mTestSurface,
+ TEST_MAX_IMAGES, TEST_IMAGE_FORMAT);
+
+ assertThat(imageWriter).isNotNull();
+ }
+}
diff --git a/camera/camera-lifecycle/build.gradle b/camera/camera-lifecycle/build.gradle
index 57fe2aa..5b54bf6 100644
--- a/camera/camera-lifecycle/build.gradle
+++ b/camera/camera-lifecycle/build.gradle
@@ -44,7 +44,7 @@
androidTestImplementation(project(":annotation:annotation-experimental"))
androidTestImplementation(project(":concurrent:concurrent-futures-ktx"))
androidTestImplementation(project(":internal-testutils-truth"))
- androidTestImplementation("org.jetbrains.kotlinx:atomicfu:0.13.1")
+ androidTestImplementation("org.jetbrains.kotlinx:atomicfu:0.15.0")
}
android {
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/Configs.java b/camera/camera-testing/src/main/java/androidx/camera/testing/Configs.java
index 951bd8f..23cffbc7 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/Configs.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/Configs.java
@@ -18,6 +18,7 @@
import androidx.annotation.NonNull;
import androidx.camera.core.UseCase;
+import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.UseCaseConfig;
import androidx.camera.core.impl.UseCaseConfigFactory;
@@ -32,12 +33,13 @@
/** Return a map that associates UseCases to UseCaseConfigs with default settings. */
@NonNull
public static Map<UseCase, UseCaseConfig<?>> useCaseConfigMapWithDefaultSettingsFromUseCaseList(
- @NonNull List<UseCase> useCases, @NonNull UseCaseConfigFactory useCaseConfigFactory) {
+ @NonNull CameraInfoInternal cameraInfo, @NonNull List<UseCase> useCases,
+ @NonNull UseCaseConfigFactory useCaseConfigFactory) {
Map<UseCase, UseCaseConfig<?>> useCaseToConfigMap = new HashMap<>();
for (UseCase useCase : useCases) {
// Combine with default configuration.
- UseCaseConfig<?> combinedUseCaseConfig = useCase.mergeConfigs(null,
+ UseCaseConfig<?> combinedUseCaseConfig = useCase.mergeConfigs(cameraInfo, null,
useCase.getDefaultConfig(true, useCaseConfigFactory));
useCaseToConfigMap.put(useCase, combinedUseCaseConfig);
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
index f020ad2..3a6ef21 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
@@ -31,8 +31,6 @@
import androidx.camera.core.impl.DeferrableSurface;
import androidx.camera.core.impl.LiveDataObservable;
import androidx.camera.core.impl.Observable;
-import androidx.camera.core.impl.Quirk;
-import androidx.camera.core.impl.Quirks;
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.UseCaseAttachState;
import androidx.camera.core.impl.utils.futures.Futures;
@@ -72,9 +70,6 @@
private List<DeferrableSurface> mConfiguredDeferrableSurfaces = Collections.emptyList();
- @NonNull
- private final List<Quirk> mCameraQuirks = Collections.emptyList();
-
public FakeCamera() {
this(DEFAULT_CAMERA_ID, /*cameraControl=*/null,
new FakeCameraInfoInternal(DEFAULT_CAMERA_ID));
@@ -300,17 +295,6 @@
return mCameraInfoInternal;
}
- @NonNull
- @Override
- public Quirks getCameraQuirks() {
- return new Quirks(mCameraQuirks);
- }
-
- /** Adds a quirk to the list of this camera's quirks. */
- public void addCameraQuirk(@NonNull final Quirk quirk) {
- mCameraQuirks.add(quirk);
- }
-
private void checkNotReleased() {
if (mState == State.RELEASED) {
throw new IllegalStateException("Camera has been released.");
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
index 19d3ab7..78aab0c 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
@@ -30,11 +30,15 @@
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
+import androidx.camera.core.impl.Quirk;
+import androidx.camera.core.impl.Quirks;
import androidx.camera.core.impl.utils.CameraOrientationUtil;
import androidx.camera.core.internal.ImmutableZoomState;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.Executor;
/**
@@ -53,6 +57,9 @@
private final MutableLiveData<ZoomState> mZoomLiveData;
private String mImplementationType = IMPLEMENTATION_TYPE_FAKE;
+ @NonNull
+ private final List<Quirk> mCameraQuirks = new ArrayList<>();
+
public FakeCameraInfoInternal() {
this(/*sensorRotation=*/ 0, /*lensFacing=*/ CameraSelector.LENS_FACING_BACK);
}
@@ -167,6 +174,17 @@
throw new UnsupportedOperationException("Not Implemented");
}
+ @NonNull
+ @Override
+ public Quirks getCameraQuirks() {
+ return new Quirks(mCameraQuirks);
+ }
+
+ /** Adds a quirk to the list of this camera's quirks. */
+ public void addCameraQuirk(@NonNull final Quirk quirk) {
+ mCameraQuirks.add(quirk);
+ }
+
/**
* Set the implementation type for testing
*/
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeImageInfo.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeImageInfo.java
index 6e97478..df8a0da 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeImageInfo.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeImageInfo.java
@@ -20,6 +20,7 @@
import androidx.camera.core.ImageInfo;
import androidx.camera.core.impl.MutableTagBundle;
import androidx.camera.core.impl.TagBundle;
+import androidx.camera.core.impl.utils.ExifData;
/**
* A fake implementation of {@link ImageInfo} where the values are settable.
@@ -62,4 +63,9 @@
public int getRotationDegrees() {
return mRotationDegrees;
}
+
+ @Override
+ public void populateExifData(@NonNull ExifData.Builder exifBuilder) {
+ exifBuilder.setOrientationDegrees(mRotationDegrees);
+ }
}
diff --git a/camera/camera-video/src/androidTest/AndroidManifest.xml b/camera/camera-video/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..9f27ec7
--- /dev/null
+++ b/camera/camera-video/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.camera.video.test">
+
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+</manifest>
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/AudioSourceTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/AudioSourceTest.kt
new file mode 100644
index 0000000..db52f2a
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/AudioSourceTest.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.video.internal
+
+import android.Manifest
+import android.media.AudioFormat
+import android.media.MediaRecorder
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.video.internal.encoder.FakeInputBuffer
+import androidx.camera.video.internal.encoder.noInvocation
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.rule.GrantPermissionRule
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+import java.util.concurrent.Callable
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class AudioSourceTest {
+
+ companion object {
+ private const val SAMPLE_RATE = 8000
+ private const val DEFAULT_MIN_BUFFER_SIZE = 1024
+ }
+
+ @get:Rule
+ var mAudioPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
+ Manifest.permission.RECORD_AUDIO
+ )
+ private lateinit var audioSource: AudioSource
+ private lateinit var fakeBufferProvider: FakeBufferProvider
+ private val bufferFactoryInvocations = mock(Callable::class.java)
+
+ @Before
+ fun setUp() {
+ fakeBufferProvider = FakeBufferProvider {
+ bufferFactoryInvocations.call()
+ FakeInputBuffer()
+ }
+ fakeBufferProvider.setActive(true)
+
+ audioSource = AudioSource.Builder()
+ .setExecutor(CameraXExecutors.ioExecutor())
+ .setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
+ .setSampleRate(SAMPLE_RATE)
+ .setChannelConfig(AudioFormat.CHANNEL_IN_MONO)
+ .setAudioFormat(AudioFormat.ENCODING_PCM_16BIT)
+ .setDefaultBufferSize(DEFAULT_MIN_BUFFER_SIZE)
+ .setBufferProvider(fakeBufferProvider)
+ .build()
+ }
+
+ @After
+ fun tearDown() {
+ if (this::audioSource.isInitialized) {
+ audioSource.release()
+ }
+ }
+
+ @Test
+ fun canRestartAudioSource() {
+ for (i in 0..2) {
+ // Act.
+ audioSource.start()
+
+ // Assert.
+ // It should continuously send audio data by invoking BufferProvider#acquireBuffer
+ verify(bufferFactoryInvocations, timeout(10000L).atLeast(3)).call()
+
+ // Act.
+ audioSource.stop()
+
+ // Assert.
+ verify(bufferFactoryInvocations, noInvocation(3000L, 6000L)).call()
+ }
+ }
+
+ @Test
+ fun bufferProviderStateChange_acquireBufferOrNot() {
+ // Arrange.
+ audioSource.start()
+
+ for (i in 0..2) {
+ // Act.
+ fakeBufferProvider.setActive(true)
+
+ // Assert.
+ // It should continuously send audio data by invoking BufferProvider#acquireBuffer
+ verify(bufferFactoryInvocations, timeout(10000L).atLeast(3)).call()
+
+ // Act.
+ fakeBufferProvider.setActive(false)
+
+ // Assert.
+ verify(bufferFactoryInvocations, noInvocation(3000L, 6000L)).call()
+ }
+ }
+}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/FakeBufferProvider.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/FakeBufferProvider.kt
new file mode 100644
index 0000000..db8e4be
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/FakeBufferProvider.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.video.internal
+
+import androidx.annotation.GuardedBy
+import androidx.camera.core.impl.Observable
+import androidx.camera.core.impl.utils.futures.Futures
+import androidx.camera.video.internal.encoder.InputBuffer
+import com.google.common.util.concurrent.ListenableFuture
+import java.lang.IllegalStateException
+import java.util.concurrent.Callable
+import java.util.concurrent.Executor
+
+class FakeBufferProvider(private val bufferFactory: Callable<InputBuffer>) :
+ BufferProvider<InputBuffer> {
+
+ private val lock = Object()
+ @GuardedBy("lock")
+ private val observers = mutableMapOf<Observable.Observer<BufferProvider.State>, Executor>()
+ @GuardedBy("lock")
+ private var state = BufferProvider.State.ACTIVE
+
+ override fun acquireBuffer(): ListenableFuture<InputBuffer> {
+ synchronized(lock) {
+ return if (state == BufferProvider.State.ACTIVE) {
+ Futures.immediateFuture(bufferFactory.call())
+ } else {
+ Futures.immediateFailedFuture(IllegalStateException("Not in ACTIVE state"))
+ }
+ }
+ }
+
+ override fun fetchData(): ListenableFuture<BufferProvider.State> {
+ synchronized(lock) {
+ return Futures.immediateFuture(state)
+ }
+ }
+
+ override fun addObserver(
+ executor: Executor,
+ observer: Observable.Observer<BufferProvider.State>
+ ) {
+ synchronized(observers) {
+ observers[observer] = executor
+ }
+ executor.execute { observer.onNewData(state) }
+ }
+
+ override fun removeObserver(observer: Observable.Observer<BufferProvider.State>) {
+ synchronized(lock) {
+ observers.remove(observer)
+ }
+ }
+
+ fun setActive(active: Boolean) {
+ val newState = if (active) BufferProvider.State.ACTIVE else BufferProvider.State.INACTIVE
+ val localObservers: Map<Observable.Observer<BufferProvider.State>, Executor>
+ synchronized(lock) {
+ if (state == newState) {
+ return
+ }
+ state = newState
+ localObservers = observers
+ }
+ for ((observer, executor) in localObservers) {
+ executor.execute { observer.onNewData(newState) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderTest.kt
index 64d1e7d..ffba5b9 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderTest.kt
@@ -16,12 +16,17 @@
package androidx.camera.video.internal.encoder
import android.media.AudioFormat
+import androidx.camera.core.impl.Observable.Observer
import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.video.internal.BufferProvider
+import androidx.camera.video.internal.BufferProvider.State
+import androidx.concurrent.futures.await
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
-import kotlinx.coroutines.Dispatchers
+import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
+import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.junit.After
@@ -37,6 +42,10 @@
import org.mockito.Mockito.verify
import org.mockito.invocation.InvocationOnMock
import java.nio.ByteBuffer
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicReference
@LargeTest
@RunWith(AndroidJUnit4::class)
@@ -51,7 +60,7 @@
private lateinit var encoder: Encoder
private lateinit var encoderCallback: EncoderCallback
- private lateinit var byteBufferProviderJob: Job
+ private lateinit var fakeAudioLoop: FakeAudioLoop
@Before
fun setup() {
@@ -74,25 +83,25 @@
)
encoder.setEncoderCallback(encoderCallback, CameraXExecutors.directExecutor())
- // Prepare a fake audio source
- val byteBuffer = ByteBuffer.allocateDirect(1024)
- byteBufferProviderJob = GlobalScope.launch(Dispatchers.Default) {
- while (true) {
- byteBuffer.rewind()
- (encoder.input as Encoder.ByteBufferInput).putByteBuffer(byteBuffer)
- delay(200)
- }
- }
+ @Suppress("UNCHECKED_CAST")
+ fakeAudioLoop = FakeAudioLoop(encoder.input as BufferProvider<InputBuffer>)
}
@After
fun tearDown() {
- encoder.release()
- byteBufferProviderJob.cancel(null)
+ if (this::encoder.isInitialized) {
+ encoder.release()
+ }
+ if (this::fakeAudioLoop.isInitialized) {
+ fakeAudioLoop.stop()
+ }
}
@Test
fun discardInputBufferBeforeStart() {
+ // Arrange.
+ fakeAudioLoop.start()
+
// Act.
// Wait a second to receive data
Thread.sleep(3000L)
@@ -103,6 +112,9 @@
@Test
fun canRestartEncoder() {
+ // Arrange.
+ fakeAudioLoop.start()
+
for (i in 0..3) {
// Arrange.
clearInvocations(encoderCallback)
@@ -125,6 +137,9 @@
@Test
fun canRestartEncoderImmediately() {
+ // Arrange.
+ fakeAudioLoop.start()
+
// Act.
encoder.start()
encoder.stop()
@@ -136,6 +151,9 @@
@Test
fun canPauseResumeEncoder() {
+ // Arrange.
+ fakeAudioLoop.start()
+
// Act.
encoder.start()
@@ -162,6 +180,9 @@
@Test
fun canPauseStopStartEncoder() {
+ // Arrange.
+ fakeAudioLoop.start()
+
// Act.
encoder.start()
@@ -191,4 +212,128 @@
// Assert.
verify(encoderCallback, timeout(15000L).atLeast(5)).onEncodedData(any())
}
+
+ @Test
+ fun bufferProvider_canAcquireBuffer() {
+ // Arrange.
+ encoder.start()
+
+ for (i in 0..8) {
+ // Act.
+ val inputBuffer = (encoder.input as Encoder.ByteBufferInput)
+ .acquireBuffer()
+ .get(3, TimeUnit.SECONDS)
+
+ // Assert.
+ assertThat(inputBuffer).isNotNull()
+ inputBuffer.cancel()
+ }
+ }
+
+ @Test
+ fun bufferProvider_canReceiveBufferProviderStateChange() {
+ // Arrange.
+ val stateRef = AtomicReference<State>()
+ val lock = Semaphore(0)
+ (encoder.input as Encoder.ByteBufferInput).addObserver(
+ CameraXExecutors.directExecutor(),
+ object : Observer<State> {
+ override fun onNewData(state: State?) {
+ stateRef.set(state)
+ lock.release()
+ }
+
+ override fun onError(t: Throwable) {
+ stateRef.set(null)
+ lock.release()
+ }
+ }
+ )
+
+ // Assert.
+ assertThat(lock.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+ assertThat(stateRef.get()).isEqualTo(State.INACTIVE)
+
+ // Act.
+ encoder.start()
+
+ // Assert.
+ assertThat(lock.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+ assertThat(stateRef.get()).isEqualTo(State.ACTIVE)
+
+ // Act.
+ encoder.pause()
+
+ // Assert
+ assertThat(lock.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+ assertThat(stateRef.get()).isEqualTo(State.INACTIVE)
+
+ // Act.
+ encoder.start()
+
+ // Assert.
+ assertThat(lock.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+ assertThat(stateRef.get()).isEqualTo(State.ACTIVE)
+
+ // Act.
+ encoder.stop()
+
+ // Assert.
+ assertThat(lock.tryAcquire(3, TimeUnit.SECONDS)).isTrue()
+ assertThat(stateRef.get()).isEqualTo(State.INACTIVE)
+ }
+
+ private class FakeAudioLoop(private val bufferProvider: BufferProvider<InputBuffer>) {
+ private val inputByteBuffer = ByteBuffer.allocateDirect(1024)
+ private val started = AtomicBoolean(false)
+ private var job: Job? = null
+
+ fun start() {
+ if (started.getAndSet(true)) {
+ return
+ }
+ job = GlobalScope.launch(
+ CameraXExecutors.ioExecutor().asCoroutineDispatcher(),
+ ) {
+ while (true) {
+ try {
+ val inputBuffer = bufferProvider.acquireBuffer().await()
+ inputBuffer.apply {
+ byteBuffer.apply {
+ put(
+ inputByteBuffer.apply {
+ clear()
+ limit(limit().coerceAtMost(byteBuffer.capacity()))
+ }
+ )
+ flip()
+ }
+ setPresentationTimeUs(System.nanoTime() / 1000L)
+ submit()
+ }
+ } catch (e: IllegalStateException) {
+ // For simplicity, AudioLoop doesn't monitor the encoder's state.
+ // When an IllegalStateException is thrown by encoder which is not started,
+ // AudioLoop should retry with a delay to avoid busy loop.
+ // CancellationException is a subclass of IllegalStateException and is
+ // ambiguous since the cancellation could be caused by ListenableFuture
+ // was cancelled or coroutine Job was cancelled. For the
+ // ListenableFuture case, AudioLoop will need to retry with a delay as
+ // IllegalStateException. For the coroutine Job case, the loop should
+ // be stopped. The goal can be simply achieved by calling delay() method
+ // because the method will also get CancellationException if it is
+ // coroutine Job cancellation, and eventually leave the audio loop.
+ delay(300L)
+ }
+ }
+ }
+ }
+
+ fun stop() {
+ if (!started.getAndSet(false)) {
+ return
+ }
+ job!!.cancel()
+ }
+ }
}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/FakeInputBuffer.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/FakeInputBuffer.kt
new file mode 100644
index 0000000..72fc1c4
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/FakeInputBuffer.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.video.internal.encoder
+
+import androidx.concurrent.futures.ResolvableFuture
+import com.google.common.util.concurrent.ListenableFuture
+import java.nio.ByteBuffer
+
+class FakeInputBuffer : InputBuffer {
+ private val byteBuffer = ByteBuffer.allocateDirect(1024)
+ private val terminationFuture = ResolvableFuture.create<Void>()
+
+ override fun getByteBuffer(): ByteBuffer {
+ throwIfTerminated()
+ return byteBuffer
+ }
+
+ override fun setPresentationTimeUs(presentationTimeUs: Long) {
+ throwIfTerminated()
+ }
+
+ override fun setEndOfStream(isEndOfStream: Boolean) {
+ throwIfTerminated()
+ }
+
+ override fun submit(): Boolean {
+ return terminationFuture.set(null)
+ }
+
+ override fun cancel(): Boolean {
+ return terminationFuture.set(null)
+ }
+
+ override fun getTerminationFuture(): ListenableFuture<Void> {
+ return terminationFuture
+ }
+
+ private fun throwIfTerminated() {
+ check(!terminationFuture.isDone)
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/AudioSource.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/AudioSource.java
new file mode 100644
index 0000000..5a08d74
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/AudioSource.java
@@ -0,0 +1,422 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal;
+
+import static androidx.camera.video.internal.AudioSource.InternalState.CONFIGURED;
+import static androidx.camera.video.internal.AudioSource.InternalState.RELEASED;
+import static androidx.camera.video.internal.AudioSource.InternalState.STARTED;
+
+import android.annotation.SuppressLint;
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.media.AudioTimestamp;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.Observable;
+import androidx.camera.core.impl.annotation.ExecutedBy;
+import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.camera.core.impl.utils.futures.FutureCallback;
+import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.camera.video.internal.encoder.InputBuffer;
+import androidx.core.util.Preconditions;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.Executor;
+
+/**
+ * AudioSource is used to obtain audio raw data and write to the buffer from {@link BufferProvider}.
+ *
+ * <p>The audio raw data could be one of sources from the device. The target source can be
+ * specified with {@link Builder#setAudioSource(int)}.
+ *
+ * <p>Calling {@link #start} will start reading audio data from the target source and then write
+ * the data into the buffer from {@link BufferProvider}. Calling {@link #stop} will stop sending
+ * audio data. However, to really read/write data to buffer, the {@link BufferProvider}'s state
+ * must be {@link BufferProvider.State#ACTIVE}. So recording may temporarily pause when the
+ * {@link BufferProvider}'s state is {@link BufferProvider.State#INACTIVE}.
+ *
+ * @see BufferProvider
+ * @see AudioRecord
+ */
+public final class AudioSource {
+ private static final String TAG = "AudioSource";
+
+ enum InternalState {
+ /** The initial state or when {@link #stop} is called after started. */
+ CONFIGURED,
+
+ /** The state is when it is in {@link #CONFIGURED} state and {@link #start} is called. */
+ STARTED,
+
+ /** The state is when {@link #release} is called. */
+ RELEASED,
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ final Executor mExecutor;
+
+ private final BufferProvider<InputBuffer> mBufferProvider;
+
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ final AudioRecord mAudioRecord;
+
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ final int mBufferSize;
+
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ InternalState mState = CONFIGURED;
+
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ BufferProvider.State mBufferProviderState = BufferProvider.State.INACTIVE;
+
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ boolean mIsSendingAudio;
+
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ AudioSource(@NonNull Executor executor,
+ @NonNull BufferProvider<InputBuffer> bufferProvider,
+ int audioSource,
+ int sampleRate,
+ int channelConfig,
+ int audioFormat,
+ int defaultBufferSize)
+ throws AudioSourceAccessException {
+ mExecutor = CameraXExecutors.newSequentialExecutor(Preconditions.checkNotNull(executor));
+ mBufferProvider = Preconditions.checkNotNull(bufferProvider);
+
+ int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
+ if (bufferSize <= 0) {
+ bufferSize = defaultBufferSize;
+ }
+ mBufferSize = bufferSize * 2;
+ try {
+ mAudioRecord = new AudioRecord(audioSource,
+ sampleRate,
+ channelConfig,
+ audioFormat,
+ mBufferSize);
+ } catch (IllegalArgumentException e) {
+ throw new AudioSourceAccessException("Unable to create AudioRecord", e);
+ }
+
+ if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
+ mAudioRecord.release();
+ throw new AudioSourceAccessException("Unable to initialize AudioRecord");
+ }
+
+ mBufferProvider.addObserver(mExecutor, mStateObserver);
+ }
+
+ /**
+ * Starts the AudioSource.
+ *
+ * <p>Audio data will start being sent to the {@link BufferProvider} when
+ * {@link BufferProvider}'s state is {@link BufferProvider.State#ACTIVE}.
+ *
+ * @throws IllegalStateException if the AudioSource is released.
+ */
+ public void start() {
+ mExecutor.execute(() -> {
+ switch (mState) {
+ case CONFIGURED:
+ setState(STARTED);
+ updateSendingAudio();
+ break;
+ case STARTED:
+ // Do nothing
+ break;
+ case RELEASED:
+ throw new IllegalStateException("AudioRecorder is released");
+ }
+ });
+ }
+
+ /**
+ * Stops the AudioSource.
+ *
+ * <p>Audio data will stop being sent to the {@link BufferProvider}.
+ *
+ * @throws IllegalStateException if it is released.
+ */
+ public void stop() {
+ mExecutor.execute(() -> {
+ switch (mState) {
+ case STARTED:
+ setState(CONFIGURED);
+ updateSendingAudio();
+ break;
+ case CONFIGURED:
+ // Do nothing
+ break;
+ case RELEASED:
+ throw new IllegalStateException("AudioRecorder is released");
+ }
+ });
+ }
+
+ /**
+ * Releases the AudioSource.
+ *
+ * <p>Once the AudioSource is released, it can not be used any more.
+ */
+ public void release() {
+ mExecutor.execute(() -> {
+ switch (mState) {
+ case STARTED:
+ case CONFIGURED:
+ mBufferProvider.removeObserver(mStateObserver);
+ mAudioRecord.release();
+ stopSendingAudio();
+ setState(RELEASED);
+ break;
+ case RELEASED:
+ // Do nothing
+ break;
+ }
+ });
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @ExecutedBy("mExecutor")
+ void updateSendingAudio() {
+ if (mState == STARTED && mBufferProviderState == BufferProvider.State.ACTIVE) {
+ startSendingAudio();
+ } else {
+ stopSendingAudio();
+ }
+ }
+
+ @ExecutedBy("mExecutor")
+ private void startSendingAudio() {
+ if (mIsSendingAudio) {
+ // Already started, ignore
+ return;
+ }
+ try {
+ Logger.d(TAG, "startSendingAudio");
+ mAudioRecord.startRecording();
+ if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
+ throw new IllegalStateException("Unable to start AudioRecord with state: "
+ + mAudioRecord.getRecordingState());
+ }
+ } catch (IllegalStateException e) {
+ Logger.w(TAG, "Failed to start AudioRecord", e);
+ return;
+ }
+ mIsSendingAudio = true;
+ sendNextAudio();
+ }
+
+ @ExecutedBy("mExecutor")
+ private void stopSendingAudio() {
+ if (!mIsSendingAudio) {
+ // Already stopped, ignore.
+ return;
+ }
+ mIsSendingAudio = false;
+ try {
+ Logger.d(TAG, "stopSendingAudio");
+ mAudioRecord.stop();
+ if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_STOPPED) {
+ throw new IllegalStateException("Unable to stop AudioRecord with state: "
+ + mAudioRecord.getRecordingState());
+ }
+ } catch (IllegalStateException e) {
+ Logger.w(TAG, "Failed to stop AudioRecord", e);
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @ExecutedBy("mExecutor")
+ void sendNextAudio() {
+ Futures.addCallback(mBufferProvider.acquireBuffer(), mAcquireBufferCallback, mExecutor);
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @ExecutedBy("mExecutor")
+ void setState(InternalState state) {
+ Logger.d(TAG, "Transitioning internal state: " + mState + " --> " + state);
+ mState = state;
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressLint("UnsafeNewApiCall")
+ long generatePresentationTimeUs() {
+ long presentationTimeUs = -1;
+ if (Build.VERSION.SDK_INT >= 24) {
+ AudioTimestamp audioTimestamp = new AudioTimestamp();
+ if (mAudioRecord.getTimestamp(audioTimestamp, AudioTimestamp.TIMEBASE_MONOTONIC)
+ == AudioRecord.SUCCESS) {
+ presentationTimeUs = audioTimestamp.nanoTime / 1000L;
+ } else {
+ Logger.w(TAG, "Unable to get audio timestamp");
+ }
+ }
+ if (presentationTimeUs == -1) {
+ presentationTimeUs = System.nanoTime() / 1000L;
+ }
+ return presentationTimeUs;
+ }
+
+ private final FutureCallback<InputBuffer> mAcquireBufferCallback =
+ new FutureCallback<InputBuffer>() {
+ @ExecutedBy("mExecutor")
+ @Override
+ public void onSuccess(InputBuffer inputBuffer) {
+ if (!mIsSendingAudio) {
+ inputBuffer.cancel();
+ return;
+ }
+ ByteBuffer byteBuffer = inputBuffer.getByteBuffer();
+
+ int length = mAudioRecord.read(byteBuffer, mBufferSize);
+ if (length > 0) {
+ byteBuffer.limit(length);
+ inputBuffer.setPresentationTimeUs(generatePresentationTimeUs());
+ inputBuffer.submit();
+ } else {
+ Logger.w(TAG, "Unable to read data from AudioRecord.");
+ inputBuffer.cancel();
+ }
+ sendNextAudio();
+ }
+
+ @ExecutedBy("mExecutor")
+ @Override
+ public void onFailure(Throwable throwable) {
+ Logger.d(TAG, "Unable to get input buffer, the BufferProvider "
+ + "could be transitioning to INACTIVE state.");
+ }
+ };
+
+ private final Observable.Observer<BufferProvider.State> mStateObserver =
+ new Observable.Observer<BufferProvider.State>() {
+ @ExecutedBy("mExecutor")
+ @Override
+ public void onNewData(@Nullable BufferProvider.State state) {
+ Logger.d(TAG, "Receive BufferProvider state change: "
+ + mBufferProviderState + " to " + state);
+ mBufferProviderState = state;
+ updateSendingAudio();
+ }
+
+ @ExecutedBy("mExecutor")
+ @Override
+ public void onError(@NonNull Throwable t) {
+ // Not define, should not be possible.
+ }
+ };
+
+ /**
+ * The builder of the AudioSource.
+ */
+ public static class Builder {
+ private Executor mExecutor;
+ private int mAudioSource;
+ private int mSampleRate;
+ private int mChannelConfig;
+ private int mAudioFormat;
+ private int mDefaultBufferSize;
+ private BufferProvider<InputBuffer> mBufferProvider;
+
+ /** Sets the executor to run the background task. */
+ @NonNull
+ public Builder setExecutor(@NonNull Executor executor) {
+ mExecutor = executor;
+ return this;
+ }
+
+ /**
+ * Sets the device audio source.
+ *
+ * @see android.media.MediaRecorder.AudioSource#MIC
+ * @see android.media.MediaRecorder.AudioSource#CAMCORDER
+ */
+ @NonNull
+ public Builder setAudioSource(int audioSource) {
+ mAudioSource = audioSource;
+ return this;
+ }
+
+ /** Sets the audio sample rate. */
+ @NonNull
+ public Builder setSampleRate(int sampleRate) {
+ mSampleRate = sampleRate;
+ return this;
+ }
+
+ /**
+ * Sets the channel config.
+ *
+ * @see AudioFormat#CHANNEL_IN_MONO
+ * @see AudioFormat#CHANNEL_IN_STEREO
+ */
+ @NonNull
+ public Builder setChannelConfig(int channelConfig) {
+ mChannelConfig = channelConfig;
+ return this;
+ }
+
+ /**
+ * Sets the audio format.
+ *
+ * @see AudioFormat#ENCODING_PCM_8BIT
+ * @see AudioFormat#ENCODING_PCM_16BIT
+ * @see AudioFormat#ENCODING_PCM_FLOAT
+ */
+ @NonNull
+ public Builder setAudioFormat(int audioFormat) {
+ mAudioFormat = audioFormat;
+ return this;
+ }
+
+ /**
+ * Sets the default buffer size.
+ *
+ * <p>AudioSource will try to generate a buffer size. But if it is unable to get one,
+ * it will apply this default buffer size.
+ */
+ @NonNull
+ public Builder setDefaultBufferSize(int bufferSize) {
+ mDefaultBufferSize = bufferSize;
+ return this;
+ }
+
+ /** Sets the {@link BufferProvider}. */
+ @NonNull
+ public Builder setBufferProvider(@NonNull BufferProvider<InputBuffer> bufferProvider) {
+ mBufferProvider = bufferProvider;
+ return this;
+ }
+
+ /** Build the AudioSource. */
+ @NonNull
+ public AudioSource build() throws AudioSourceAccessException {
+ return new AudioSource(mExecutor,
+ mBufferProvider,
+ mAudioSource,
+ mSampleRate,
+ mChannelConfig,
+ mAudioFormat,
+ mDefaultBufferSize
+ );
+ }
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/MalformedVersionException.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/AudioSourceAccessException.java
similarity index 63%
rename from car/app/app/src/main/java/androidx/car/app/MalformedVersionException.java
rename to camera/camera-video/src/main/java/androidx/camera/video/internal/AudioSourceAccessException.java
index b685ecb..204ba20 100644
--- a/car/app/app/src/main/java/androidx/car/app/MalformedVersionException.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/AudioSourceAccessException.java
@@ -14,24 +14,22 @@
* limitations under the License.
*/
-package androidx.car.app;
+package androidx.camera.video.internal;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-/**
- * An exception for malformed {@link CarAppVersion} strings.
- */
-public class MalformedVersionException extends Exception {
- public MalformedVersionException(@Nullable String message) {
+/** An exception thrown to indicate an error has occurred during configuring an audio source. */
+public class AudioSourceAccessException extends Exception {
+
+ public AudioSourceAccessException(@Nullable String message) {
super(message);
}
- public MalformedVersionException(@NonNull String message, @NonNull Throwable cause) {
+ public AudioSourceAccessException(@Nullable String message, @Nullable Throwable cause) {
super(message, cause);
}
- public MalformedVersionException(@Nullable Throwable cause) {
+ public AudioSourceAccessException(@Nullable Throwable cause) {
super(cause);
}
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/BufferProvider.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/BufferProvider.java
new file mode 100644
index 0000000..1dfd4c1
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/BufferProvider.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.impl.Observable;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * BufferProvider provides buffers for writing data.
+ *
+ * <p>BufferProvider has {@link State}, it could be either {@link State#ACTIVE} or
+ * {@link State#INACTIVE}. The state can be fetched directly through {@link #fetchData()} or use
+ * {@link #addObserver} to receive state changes.
+ *
+ * <p>A buffer for writing data can be acquired with {@link #acquireBuffer()}". The buffer can
+ * only be obtained when the state is {@link State#ACTIVE}. If the state is
+ * {@link State#INACTIVE}, the {@link #acquireBuffer()} will return a failed
+ * {@link ListenableFuture} with {@link IllegalStateException}. If the state is transitioned from
+ * {@link State#ACTIVE} to {@link State#INACTIVE}, the incomplete {@link ListenableFuture} will
+ * get {@link java.util.concurrent.CancellationException}. Buffer acquisition can be cancelled
+ * with {@link ListenableFuture#cancel} if acquisition is not yet complete.
+ *
+ * @param <T> the buffer data type
+ */
+public interface BufferProvider<T> extends Observable<BufferProvider.State> {
+
+ /**
+ * Acquires a buffer.
+ *
+ * <p>A buffer can only be obtained when the state is {@link State#ACTIVE}. If the state is
+ * {@link State#INACTIVE}, the {@link #acquireBuffer()} will return an failed
+ * {@link ListenableFuture} with {@link IllegalStateException}. If the state is transitioned
+ * from {@link State#ACTIVE} to {@link State#INACTIVE}, the incomplete
+ * {@link ListenableFuture} will get {@link java.util.concurrent.CancellationException}.
+ * Buffer acquisition can be cancelled with {@link ListenableFuture#cancel} if acquisition
+ * is not yet complete.
+ *
+ * @return a {@link ListenableFuture} to represent the acquisition.
+ */
+ @NonNull
+ ListenableFuture<T> acquireBuffer();
+
+ /** The state of the BufferProvider. */
+ enum State {
+
+ /** The state means it is able to acquire a buffer. */
+ ACTIVE,
+
+ /**
+ * The state means it is not able to acquire buffer.
+ *
+ * <p>The acquisition via {@link #acquireBuffer()} will get a result with
+ * {@link IllegalStateException}.
+ */
+ INACTIVE,
+ }
+}
+
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncodedDataImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncodedDataImpl.java
index 723edce..1cef3f9 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncodedDataImpl.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncodedDataImpl.java
@@ -88,7 +88,7 @@
}
try {
mMediaCodec.releaseOutputBuffer(mBufferIndex, false);
- } catch (MediaCodec.CodecException e) {
+ } catch (IllegalStateException e) {
mClosedCompleter.setException(e);
return;
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/Encoder.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/Encoder.java
index da6ebce..d6fa8a5 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/Encoder.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/Encoder.java
@@ -19,8 +19,8 @@
import android.view.Surface;
import androidx.annotation.NonNull;
+import androidx.camera.video.internal.BufferProvider;
-import java.nio.ByteBuffer;
import java.util.concurrent.Executor;
/**
@@ -108,18 +108,13 @@
}
}
- /** A ByteBufferInput provides {@link #putByteBuffer} method to send raw data. */
- interface ByteBufferInput extends EncoderInput {
-
- /**
- * Puts an input raw {@link ByteBuffer} to the encoder.
- *
- * <p>The input {@code ByteBuffer} must be put when encoder is in started and not paused
- * state, otherwise the {@code ByteBuffer} will be dropped directly. Then the encoded data
- * will be sent via {@link EncoderCallback#onEncodedData} callback.
- *
- * @param byteBuffer the input byte buffer
- */
- void putByteBuffer(@NonNull ByteBuffer byteBuffer);
+ /**
+ * A ByteBufferInput is a {@link BufferProvider} implementation and provides
+ * {@link InputBuffer} to write input data to the encoder.
+ *
+ * @see BufferProvider
+ * @see InputBuffer
+ */
+ interface ByteBufferInput extends EncoderInput, BufferProvider<InputBuffer> {
}
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
index 00280df..acd7535 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
@@ -17,6 +17,7 @@
package androidx.camera.video.internal.encoder;
import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.CONFIGURED;
+import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.ERROR;
import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.PAUSED;
import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.PENDING_RELEASE;
import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.PENDING_START;
@@ -36,24 +37,29 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.Logger;
+import androidx.camera.core.impl.Observable;
+import androidx.camera.core.impl.annotation.ExecutedBy;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures;
-import androidx.core.util.Consumer;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
import androidx.core.util.Preconditions;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.IOException;
-import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.atomic.AtomicReference;
/**
* The encoder implementation.
@@ -106,20 +112,34 @@
*/
PENDING_RELEASE,
+ /**
+ * Then state is when the encoder encounter error. Error state is a transitional state
+ * where encoder user is supposed to wait for {@link EncoderCallback#onEncodeStop} or
+ * {@link EncoderCallback#onEncodeError}. Any method call during this state should be
+ * ignore except {@link #release}.
+ */
+ ERROR,
+
/** The state is when the encoder is released. */
RELEASED,
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final Object mLock = new Object();
+ private final InternalStateObservable mStateObservable = new InternalStateObservable();
private final MediaFormat mMediaFormat;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
- @GuardedBy("mLock")
final MediaCodec mMediaCodec;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final EncoderInput mEncoderInput;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
- final Executor mExecutor;
+ final Executor mEncoderExecutor;
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ final Queue<Integer> mFreeInputBufferIndexQueue = new ArrayDeque<>();
+ private final Queue<Completer<InputBuffer>> mAcquisitionQueue = new ArrayDeque<>();
+ private final Set<InputBuffer> mInputBufferSet = new HashSet<>();
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ final Set<EncodedDataImpl> mEncodedDataSet = new HashSet<>();
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@GuardedBy("mLock")
@@ -128,7 +148,6 @@
@GuardedBy("mLock")
Executor mEncoderCallbackExecutor = CameraXExecutors.mainThreadExecutor();
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
- @GuardedBy("mLock")
InternalState mState;
/**
@@ -143,10 +162,26 @@
Preconditions.checkNotNull(executor);
Preconditions.checkNotNull(encoderConfig);
- mExecutor = CameraXExecutors.newSequentialExecutor(executor);
+ mEncoderExecutor = CameraXExecutors.newSequentialExecutor(executor);
if (encoderConfig instanceof AudioEncoderConfig) {
- mEncoderInput = new ByteBufferInput();
+ ByteBufferInput byteBufferInput = new ByteBufferInput();
+ // State change will run on mEncoderExecutor, use direct executor to avoid delay.
+ mStateObservable.addObserver(CameraXExecutors.directExecutor(),
+ new Observable.Observer<InternalState>() {
+ @ExecutedBy("mEncoderExecutor")
+ @Override
+ public void onNewData(@Nullable InternalState value) {
+ byteBufferInput.setActive(value == STARTED);
+ }
+
+ @ExecutedBy("mEncoderExecutor")
+ @Override
+ public void onError(@NonNull Throwable t) {
+ // No defined. Ignore.
+ }
+ });
+ mEncoderInput = byteBufferInput;
} else if (encoderConfig instanceof VideoEncoderConfig) {
mEncoderInput = new SurfaceInput();
} else {
@@ -171,18 +206,22 @@
setState(CONFIGURED);
}
- @SuppressWarnings("GuardedBy")
- // It complains SurfaceInput#resetSurface and ByteBufferInput#clearFreeBuffers don't hold mLock
- @GuardedBy("mLock")
+ @ExecutedBy("mEncoderExecutor")
private void reset() {
+ mFreeInputBufferIndexQueue.clear();
+
+ // Cancel incomplete acquisitions if exists.
+ for (Completer<InputBuffer> completer : mAcquisitionQueue) {
+ completer.setCancelled();
+ }
+ mAcquisitionQueue.clear();
+
mMediaCodec.reset();
mMediaCodec.setCallback(new MediaCodecCallback());
mMediaCodec.configure(mMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
if (mEncoderInput instanceof SurfaceInput) {
((SurfaceInput) mEncoderInput).resetSurface();
- } else if (mEncoderInput instanceof ByteBufferInput) {
- ((ByteBufferInput) mEncoderInput).clearFreeBuffers();
}
}
@@ -204,7 +243,7 @@
*/
@Override
public void start() {
- synchronized (mLock) {
+ mEncoderExecutor.execute(() -> {
switch (mState) {
case CONFIGURED:
try {
@@ -222,6 +261,7 @@
setState(STARTED);
break;
case STARTED:
+ case ERROR:
case PENDING_START:
// Do nothing
break;
@@ -235,7 +275,7 @@
default:
throw new IllegalStateException("Unknown state: " + mState);
}
- }
+ });
}
/**
@@ -244,21 +284,26 @@
* <p>It will trigger {@link EncoderCallback#onEncodeStop} after the last encoded data. It can
* call {@link #start} to start again.
*/
- @SuppressWarnings("GuardedBy")
- // It complains ByteBufferInput#signalEndOfInputStream doesn't hold mLock
@Override
public void stop() {
- synchronized (mLock) {
+ mEncoderExecutor.execute(() -> {
switch (mState) {
case CONFIGURED:
case STOPPING:
+ case ERROR:
// Do nothing
break;
case STARTED:
case PAUSED:
setState(STOPPING);
if (mEncoderInput instanceof ByteBufferInput) {
- ((ByteBufferInput) mEncoderInput).signalEndOfInputStream();
+ // Wait for all issued input buffer done to avoid input loss.
+ List<ListenableFuture<Void>> futures = new ArrayList<>();
+ for (InputBuffer inputBuffer : mInputBufferSet) {
+ futures.add(inputBuffer.getTerminationFuture());
+ }
+ Futures.successfulAsList(futures).addListener(this::signalEndOfInputStream,
+ mEncoderExecutor);
} else if (mEncoderInput instanceof SurfaceInput) {
try {
mMediaCodec.signalEndOfInputStream();
@@ -277,7 +322,7 @@
default:
throw new IllegalStateException("Unknown state: " + mState);
}
- }
+ });
}
/**
@@ -288,10 +333,11 @@
*/
@Override
public void pause() {
- synchronized (mLock) {
+ mEncoderExecutor.execute(() -> {
switch (mState) {
case CONFIGURED:
case PAUSED:
+ case ERROR:
case STOPPING:
case PENDING_START_PAUSED:
// Do nothing
@@ -311,7 +357,7 @@
default:
throw new IllegalStateException("Unknown state: " + mState);
}
- }
+ });
}
/**
@@ -324,11 +370,12 @@
*/
@Override
public void release() {
- synchronized (mLock) {
+ mEncoderExecutor.execute(() -> {
switch (mState) {
case CONFIGURED:
case STARTED:
case PAUSED:
+ case ERROR:
mMediaCodec.release();
setState(RELEASED);
break;
@@ -344,7 +391,7 @@
default:
throw new IllegalStateException("Unknown state: " + mState);
}
- }
+ });
}
/**
@@ -363,42 +410,142 @@
}
}
- @GuardedBy("mLock")
+ @ExecutedBy("mEncoderExecutor")
private void setState(InternalState state) {
+ if (mState == state) {
+ return;
+ }
Logger.d(TAG, "Transitioning encoder internal state: " + mState + " --> " + state);
mState = state;
+ mStateObservable.notifyState();
}
- @GuardedBy("mLock")
+ @ExecutedBy("mEncoderExecutor")
private void updatePauseToMediaCodec(boolean paused) {
Bundle bundle = new Bundle();
bundle.putBoolean(MediaCodec.PARAMETER_KEY_SUSPEND, paused);
mMediaCodec.setParameters(bundle);
}
+ @ExecutedBy("mEncoderExecutor")
+ private void signalEndOfInputStream() {
+ Futures.addCallback(acquireInputBuffer(),
+ new FutureCallback<InputBuffer>() {
+ @Override
+ public void onSuccess(InputBuffer inputBuffer) {
+ inputBuffer.setPresentationTimeUs(generatePresentationTimeUs());
+ inputBuffer.setEndOfStream(true);
+ inputBuffer.submit();
+
+ Futures.addCallback(inputBuffer.getTerminationFuture(),
+ new FutureCallback<Void>() {
+ @ExecutedBy("mEncoderExecutor")
+ @Override
+ public void onSuccess(@Nullable Void result) {
+ // Do nothing.
+ }
+
+ @ExecutedBy("mEncoderExecutor")
+ @Override
+ public void onFailure(Throwable t) {
+ if (t instanceof MediaCodec.CodecException) {
+ handleEncodeError(
+ (MediaCodec.CodecException) t);
+ } else {
+ handleEncodeError(EncodeException.ERROR_UNKNOWN,
+ t.getMessage(), t);
+ }
+ }
+ }, mEncoderExecutor);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ handleEncodeError(EncodeException.ERROR_UNKNOWN,
+ "Unable to acquire InputBuffer.", t);
+ }
+ }, mEncoderExecutor);
+ }
+
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
- @GuardedBy("mLock")
+ @ExecutedBy("mEncoderExecutor")
void handleEncodeError(@NonNull MediaCodec.CodecException e) {
handleEncodeError(EncodeException.ERROR_CODEC, e.getMessage(), e);
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
- @GuardedBy("mLock")
+ @ExecutedBy("mEncoderExecutor")
void handleEncodeError(@EncodeException.ErrorType int error, @Nullable String message,
@Nullable Throwable throwable) {
- EncoderCallback encoderCallback = mEncoderCallback;
- try {
- mEncoderCallbackExecutor.execute(() -> encoderCallback.onEncodeError(
- new EncodeException(error, message, throwable)));
- } catch (RejectedExecutionException re) {
- Logger.e(TAG, "Unable to post to the supplied executor.", re);
+ switch (mState) {
+ case CONFIGURED:
+ // Unable to start MediaCodec. This is a fatal error. Try to reset the encoder.
+ notifyError(error, message, throwable);
+ reset();
+ break;
+ case STARTED:
+ case PAUSED:
+ case STOPPING:
+ case PENDING_START_PAUSED:
+ case PENDING_START:
+ case PENDING_RELEASE:
+ setState(ERROR);
+ stopMediaCodec(() -> notifyError(error, message, throwable));
+ break;
+ case ERROR:
+ Logger.w(TAG, "Get more than one error: " + message + "(" + error + ")",
+ throwable);
+ break;
+ case RELEASED:
+ // Do nothing
+ break;
}
- mMediaCodec.stop();
- handleStopped();
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
- @GuardedBy("mLock")
+ void notifyError(@EncodeException.ErrorType int error, @Nullable String message,
+ @Nullable Throwable throwable) {
+ EncoderCallback callback;
+ Executor executor;
+ synchronized (mLock) {
+ callback = mEncoderCallback;
+ executor = mEncoderCallbackExecutor;
+ }
+ try {
+ executor.execute(
+ () -> callback.onEncodeError(new EncodeException(error, message, throwable)));
+ } catch (RejectedExecutionException e) {
+ Logger.e(TAG, "Unable to post to the supplied executor.", e);
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @ExecutedBy("mEncoderExecutor")
+ void stopMediaCodec(@Nullable Runnable afterStop) {
+ /*
+ * MediaCodec#close will free all its input/output ByteBuffers. Therefore, before calling
+ * MediaCodec#close, it must ensure all dispatched EncodedData(output ByteBuffers) and
+ * InputBuffer(input ByteBuffers) are complete. Otherwise, the ByteBuffer receiver will
+ * get buffer overflow when accessing the ByteBuffers.
+ */
+ List<ListenableFuture<Void>> futures = new ArrayList<>();
+ for (EncodedDataImpl dataToClose : mEncodedDataSet) {
+ futures.add(dataToClose.getClosedFuture());
+ }
+ for (InputBuffer inputBuffer : mInputBufferSet) {
+ futures.add(inputBuffer.getTerminationFuture());
+ }
+ Futures.successfulAsList(futures).addListener(() -> {
+ mMediaCodec.stop();
+ if (afterStop != null) {
+ afterStop.run();
+ }
+ handleStopped();
+ }, mEncoderExecutor);
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @ExecutedBy("mEncoderExecutor")
void handleStopped() {
if (mState == PENDING_RELEASE) {
mMediaCodec.release();
@@ -417,25 +564,115 @@
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @ExecutedBy("mEncoderExecutor")
+ @NonNull
+ ListenableFuture<InputBuffer> acquireInputBuffer() {
+ switch (mState) {
+ case CONFIGURED:
+ return Futures.immediateFailedFuture(new IllegalStateException(
+ "Encoder is not started yet."));
+ case STARTED:
+ case PAUSED:
+ case STOPPING:
+ case PENDING_START:
+ case PENDING_START_PAUSED:
+ case PENDING_RELEASE:
+ AtomicReference<Completer<InputBuffer>> ref = new AtomicReference<>();
+ ListenableFuture<InputBuffer> future = CallbackToFutureAdapter.getFuture(
+ completer -> {
+ ref.set(completer);
+ return "acquireInputBuffer";
+ });
+ Completer<InputBuffer> completer = Preconditions.checkNotNull(ref.get());
+ mAcquisitionQueue.offer(completer);
+ completer.addCancellationListener(() -> mAcquisitionQueue.remove(completer),
+ mEncoderExecutor);
+ matchAcquisitionsAndFreeBufferIndexes();
+ return future;
+ case ERROR:
+ return Futures.immediateFailedFuture(new IllegalStateException(
+ "Encoder is in error state."));
+ case RELEASED:
+ return Futures.immediateFailedFuture(new IllegalStateException(
+ "Encoder is released."));
+ default:
+ throw new IllegalStateException("Unknown state: " + mState);
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @ExecutedBy("mEncoderExecutor")
+ void matchAcquisitionsAndFreeBufferIndexes() {
+ while (!mAcquisitionQueue.isEmpty() && !mFreeInputBufferIndexQueue.isEmpty()) {
+ Completer<InputBuffer> completer = mAcquisitionQueue.poll();
+ int bufferIndex = mFreeInputBufferIndexQueue.poll();
+
+ InputBufferImpl inputBuffer;
+ try {
+ inputBuffer = new InputBufferImpl(mMediaCodec, bufferIndex);
+ } catch (MediaCodec.CodecException e) {
+ handleEncodeError(e);
+ return;
+ }
+ if (completer.set(inputBuffer)) {
+ mInputBufferSet.add(inputBuffer);
+ inputBuffer.getTerminationFuture().addListener(
+ () -> mInputBufferSet.remove(inputBuffer), mEncoderExecutor);
+ } else {
+ inputBuffer.cancel();
+ }
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
static long generatePresentationTimeUs() {
return System.nanoTime() / 1000L;
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ class InternalStateObservable implements Observable<InternalState> {
+
+ private final Map<Observer<InternalState>, Executor> mObservers = new LinkedHashMap<>();
+
+ @ExecutedBy("mEncoderExecutor")
+ void notifyState() {
+ final InternalState state = mState;
+ for (Map.Entry<Observer<InternalState>, Executor> entry : mObservers.entrySet()) {
+ entry.getValue().execute(() -> entry.getKey().onNewData(state));
+ }
+ }
+
+ @ExecutedBy("mEncoderExecutor")
+ @NonNull
+ @Override
+ public ListenableFuture<InternalState> fetchData() {
+ return Futures.immediateFuture(mState);
+ }
+
+ @ExecutedBy("mEncoderExecutor")
+ @Override
+ public void addObserver(@NonNull Executor executor,
+ @NonNull Observer<InternalState> observer) {
+ final InternalState state = mState;
+ mObservers.put(observer, executor);
+ executor.execute(() -> observer.onNewData(state));
+ }
+
+ @ExecutedBy("mEncoderExecutor")
+ @Override
+ public void removeObserver(@NonNull Observer<InternalState> observer) {
+ mObservers.remove(observer);
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
class MediaCodecCallback extends MediaCodec.Callback {
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
- @GuardedBy("mLock")
- final Set<EncodedDataImpl> mEncodedDataSet = new HashSet<>();
-
- @GuardedBy("mLock")
private boolean mHasFirstData = false;
- @SuppressWarnings("GuardedBy")
- // It complains ByteBufferInput#putFreeBufferIndex doesn't hold mLock
@Override
public void onInputBufferAvailable(MediaCodec mediaCodec, int index) {
- synchronized (mLock) {
+ mEncoderExecutor.execute(() -> {
switch (mState) {
case STARTED:
case PAUSED:
@@ -443,24 +680,24 @@
case PENDING_START:
case PENDING_START_PAUSED:
case PENDING_RELEASE:
- if (mEncoderInput instanceof ByteBufferInput) {
- ((ByteBufferInput) mEncoderInput).putFreeBufferIndex(index);
- }
+ mFreeInputBufferIndexQueue.offer(index);
+ matchAcquisitionsAndFreeBufferIndexes();
break;
case CONFIGURED:
+ case ERROR:
case RELEASED:
// Do nothing
break;
default:
throw new IllegalStateException("Unknown state: " + mState);
}
- }
+ });
}
@Override
public void onOutputBufferAvailable(@NonNull MediaCodec mediaCodec, int index,
@NonNull MediaCodec.BufferInfo bufferInfo) {
- synchronized (mLock) {
+ mEncoderExecutor.execute(() -> {
switch (mState) {
case STARTED:
case PAUSED:
@@ -468,8 +705,12 @@
case PENDING_START:
case PENDING_START_PAUSED:
case PENDING_RELEASE:
- final EncoderCallback encoderCallback = mEncoderCallback;
- final Executor executor = mEncoderCallbackExecutor;
+ final EncoderCallback encoderCallback;
+ final Executor executor;
+ synchronized (mLock) {
+ encoderCallback = mEncoderCallback;
+ executor = mEncoderCallbackExecutor;
+ }
// Handle start of stream
if (!mHasFirstData) {
@@ -507,25 +748,21 @@
new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
- synchronized (mLock) {
- mEncodedDataSet.remove(encodedData);
- }
+ mEncodedDataSet.remove(encodedData);
}
@Override
public void onFailure(Throwable t) {
- synchronized (mLock) {
- mEncodedDataSet.remove(encodedData);
- if (t instanceof MediaCodec.CodecException) {
- handleEncodeError(
- (MediaCodec.CodecException) t);
- } else {
- handleEncodeError(EncodeException.ERROR_UNKNOWN,
- t.getMessage(), t);
- }
+ mEncodedDataSet.remove(encodedData);
+ if (t instanceof MediaCodec.CodecException) {
+ handleEncodeError(
+ (MediaCodec.CodecException) t);
+ } else {
+ handleEncodeError(EncodeException.ERROR_UNKNOWN,
+ t.getMessage(), t);
}
}
- }, CameraXExecutors.directExecutor());
+ }, mEncoderExecutor);
try {
executor.execute(() -> encoderCallback.onEncodedData(encodedData));
} catch (RejectedExecutionException e) {
@@ -543,56 +780,29 @@
// Handle end of stream
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
- // Wait for all data closed
- List<ListenableFuture<Void>> waitForCloseFutures = new ArrayList<>();
- for (EncodedDataImpl dataToClose : mEncodedDataSet) {
- waitForCloseFutures.add(dataToClose.getClosedFuture());
- }
- Futures.addCallback(Futures.allAsList(waitForCloseFutures),
- new FutureCallback<List<Void>>() {
- @Override
- public void onSuccess(@Nullable List<Void> result) {
- synchronized (mLock) {
- mMediaCodec.stop();
- try {
- executor.execute(encoderCallback::onEncodeStop);
- } catch (RejectedExecutionException e) {
- Logger.e(TAG,
- "Unable to post to the supplied "
- + "executor.", e);
- }
- handleStopped();
- }
- }
-
- @Override
- public void onFailure(Throwable t) {
- synchronized (mLock) {
- if (t instanceof MediaCodec.CodecException) {
- handleEncodeError(
- (MediaCodec.CodecException) t);
- } else {
- handleEncodeError(EncodeException.ERROR_UNKNOWN,
- t.getMessage(), t);
- }
- }
- }
- }, CameraXExecutors.directExecutor());
+ stopMediaCodec(() -> {
+ try {
+ executor.execute(encoderCallback::onEncodeStop);
+ } catch (RejectedExecutionException e) {
+ Logger.e(TAG, "Unable to post to the supplied executor.", e);
+ }
+ });
}
break;
case CONFIGURED:
+ case ERROR:
case RELEASED:
// Do nothing
break;
default:
throw new IllegalStateException("Unknown state: " + mState);
}
- }
+ });
}
@Override
public void onError(@NonNull MediaCodec mediaCodec, @NonNull MediaCodec.CodecException e) {
- synchronized (mLock) {
+ mEncoderExecutor.execute(() -> {
switch (mState) {
case STARTED:
case PAUSED:
@@ -603,19 +813,20 @@
handleEncodeError(e);
break;
case CONFIGURED:
+ case ERROR:
case RELEASED:
// Do nothing
break;
default:
throw new IllegalStateException("Unknown state: " + mState);
}
- }
+ });
}
@Override
public void onOutputFormatChanged(@NonNull MediaCodec mediaCodec,
@NonNull MediaFormat mediaFormat) {
- synchronized (mLock) {
+ mEncoderExecutor.execute(() -> {
switch (mState) {
case STARTED:
case PAUSED:
@@ -623,28 +834,36 @@
case PENDING_START:
case PENDING_START_PAUSED:
case PENDING_RELEASE:
- EncoderCallback encoderCallback = mEncoderCallback;
+ EncoderCallback encoderCallback;
+ Executor executor;
+ synchronized (mLock) {
+ encoderCallback = mEncoderCallback;
+ executor = mEncoderCallbackExecutor;
+ }
try {
- mEncoderCallbackExecutor.execute(
+ executor.execute(
() -> encoderCallback.onOutputConfigUpdate(() -> mediaFormat));
} catch (RejectedExecutionException e) {
Logger.e(TAG, "Unable to post to the supplied executor.", e);
}
break;
case CONFIGURED:
+ case ERROR:
case RELEASED:
// Do nothing
break;
default:
throw new IllegalStateException("Unknown state: " + mState);
}
- }
+ });
}
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
class SurfaceInput implements Encoder.SurfaceInput {
+ private final Object mLock = new Object();
+
@GuardedBy("mLock")
private Surface mSurface;
@@ -663,42 +882,50 @@
@Override
public void setOnSurfaceUpdateListener(@NonNull Executor executor,
@NonNull OnSurfaceUpdateListener listener) {
+ Surface surface;
synchronized (mLock) {
mSurfaceUpdateListener = Preconditions.checkNotNull(listener);
mSurfaceUpdateExecutor = Preconditions.checkNotNull(executor);
-
- if (mSurface != null) {
- notifySurfaceUpdate(mSurface);
- }
-
+ surface = mSurface;
+ }
+ if (surface != null) {
+ notifySurfaceUpdate(executor, listener, surface);
}
}
- @GuardedBy("mLock")
@SuppressLint("UnsafeNewApiCall")
void resetSurface() {
- if (Build.VERSION.SDK_INT >= 23) {
- if (mSurface == null) {
- mSurface = MediaCodec.createPersistentInputSurface();
- notifySurfaceUpdate(mSurface);
+ Surface surface;
+ Executor executor;
+ OnSurfaceUpdateListener listener;
+ synchronized (mLock) {
+ if (Build.VERSION.SDK_INT >= 23) {
+ if (mSurface == null) {
+ mSurface = MediaCodec.createPersistentInputSurface();
+ surface = mSurface;
+ } else {
+ surface = null;
+ }
+ mMediaCodec.setInputSurface(mSurface);
+ } else {
+ mSurface = mMediaCodec.createInputSurface();
+ surface = mSurface;
}
- mMediaCodec.setInputSurface(mSurface);
- } else {
- mSurface = mMediaCodec.createInputSurface();
- notifySurfaceUpdate(mSurface);
+ listener = mSurfaceUpdateListener;
+ executor = mSurfaceUpdateExecutor;
+ }
+ if (surface != null && listener != null && executor != null) {
+ notifySurfaceUpdate(executor, listener, surface);
}
}
- @GuardedBy("mLock")
- private void notifySurfaceUpdate(@NonNull Surface surface) {
- if (mSurfaceUpdateListener != null && mSurfaceUpdateExecutor != null) {
- OnSurfaceUpdateListener listener = mSurfaceUpdateListener;
- try {
- mSurfaceUpdateExecutor.execute(() -> listener.onSurfaceUpdate(surface));
- } catch (RejectedExecutionException e) {
- Logger.e(TAG, "Unable to post to the supplied executor.", e);
- surface.release();
- }
+ private void notifySurfaceUpdate(@NonNull Executor executor,
+ @NonNull OnSurfaceUpdateListener listener, @NonNull Surface surface) {
+ try {
+ executor.execute(() -> listener.onSurfaceUpdate(surface));
+ } catch (RejectedExecutionException e) {
+ Logger.e(TAG, "Unable to post to the supplied executor.", e);
+ surface.release();
}
}
}
@@ -706,139 +933,91 @@
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
class ByteBufferInput implements Encoder.ByteBufferInput {
- @GuardedBy("mLock")
- private final Queue<Consumer<Integer>> mListenerQueue = new ArrayDeque<>();
+ private final Map<Observer<State>, Executor> mStateObservers = new LinkedHashMap<>();
- @GuardedBy("mLock")
- private final Queue<Integer> mFreeBufferIndexQueue = new ArrayDeque<>();
+ private State mBufferProviderState = State.INACTIVE;
+
+ private final List<ListenableFuture<InputBuffer>> mAcquisitionList = new ArrayList<>();
/** {@inheritDoc} */
+ @NonNull
@Override
- public void putByteBuffer(@NonNull ByteBuffer byteBuffer) {
- synchronized (mLock) {
- switch (mState) {
- case STARTED:
- // Here it means the byteBuffer should definitely be queued into codec.
- acquireFreeBufferIndex(freeBufferIndex -> {
- ByteBuffer inputBuffer = null;
- synchronized (mLock) {
- if (mState == STARTED
- || mState == PAUSED
- || mState == STOPPING
- || mState == PENDING_START
- || mState == PENDING_START_PAUSED
- || mState == PENDING_RELEASE) {
- try {
- inputBuffer = mMediaCodec.getInputBuffer(freeBufferIndex);
- } catch (MediaCodec.CodecException e) {
- handleEncodeError(e);
- return;
- }
- }
- }
-
- if (inputBuffer == null) {
- return;
- }
- inputBuffer.put(byteBuffer);
-
- synchronized (mLock) {
- if (mState == STARTED
- || mState == PAUSED
- || mState == STOPPING
- || mState == PENDING_START
- || mState == PENDING_START_PAUSED
- || mState == PENDING_RELEASE) {
- try {
- mMediaCodec.queueInputBuffer(freeBufferIndex, 0,
- inputBuffer.position(),
- generatePresentationTimeUs(),
- 0);
- } catch (MediaCodec.CodecException e) {
- handleEncodeError(e);
- return;
- }
- }
- }
- });
- break;
- case PAUSED:
- // Drop the data
- break;
- case CONFIGURED:
- case STOPPING:
- case PENDING_START:
- case PENDING_RELEASE:
- case RELEASED:
- // Do nothing
- break;
- default:
- throw new IllegalStateException("Unknown state: " + mState);
- }
- }
- }
-
- @GuardedBy("mLock")
- void signalEndOfInputStream() {
- acquireFreeBufferIndex(freeBufferIndex -> {
- synchronized (mLock) {
- switch (mState) {
- case STARTED:
- case PAUSED:
- case STOPPING:
- case PENDING_START:
- case PENDING_START_PAUSED:
- case PENDING_RELEASE:
- try {
- mMediaCodec.queueInputBuffer(freeBufferIndex, 0, 0,
- generatePresentationTimeUs(),
- MediaCodec.BUFFER_FLAG_END_OF_STREAM);
- } catch (MediaCodec.CodecException e) {
- handleEncodeError(e);
- }
- break;
- case CONFIGURED:
- case RELEASED:
- // Do nothing
- break;
- default:
- throw new IllegalStateException("Unknown state: " + mState);
- }
- }
+ public ListenableFuture<State> fetchData() {
+ return CallbackToFutureAdapter.getFuture(completer -> {
+ mEncoderExecutor.execute(() -> completer.set(mBufferProviderState));
+ return "fetchData";
});
}
- @GuardedBy("mLock")
- void putFreeBufferIndex(int index) {
- mFreeBufferIndexQueue.offer(index);
- match();
+ /** {@inheritDoc} */
+ @NonNull
+ @Override
+ public ListenableFuture<InputBuffer> acquireBuffer() {
+ return CallbackToFutureAdapter.getFuture(completer -> {
+ mEncoderExecutor.execute(() -> {
+ if (mBufferProviderState == State.ACTIVE) {
+ ListenableFuture<InputBuffer> future = acquireInputBuffer();
+ Futures.propagate(future, completer);
+ // Cancel by outer, also cancel internal future.
+ completer.addCancellationListener(() -> future.cancel(true),
+ CameraXExecutors.directExecutor());
+
+ // Keep tracking the acquisition by internal future. Once the provider state
+ // transition to inactive, cancel the internal future can also send signal
+ // to outer future since we propagate the internal result to the completer.
+ mAcquisitionList.add(future);
+ future.addListener(() -> mAcquisitionList.remove(future), mEncoderExecutor);
+ } else if (mBufferProviderState == State.INACTIVE) {
+ completer.setException(
+ new IllegalStateException("BufferProvider is not active."));
+ } else {
+ completer.setException(
+ new IllegalStateException(
+ "Unknown state: " + mBufferProviderState));
+ }
+ });
+ return "acquireBuffer";
+ });
}
- @GuardedBy("mLock")
- void clearFreeBuffers() {
- mListenerQueue.clear();
- mFreeBufferIndexQueue.clear();
+ /** {@inheritDoc} */
+ @Override
+ public void addObserver(@NonNull Executor executor, @NonNull Observer<State> observer) {
+ mEncoderExecutor.execute(() -> {
+ mStateObservers.put(Preconditions.checkNotNull(observer),
+ Preconditions.checkNotNull(executor));
+ final State state = mBufferProviderState;
+ executor.execute(() -> observer.onNewData(state));
+ });
}
- @GuardedBy("mLock")
- private void acquireFreeBufferIndex(
- @NonNull Consumer<Integer> onFreeBufferIndexListener) {
- synchronized (mLock) {
- mListenerQueue.offer(onFreeBufferIndexListener);
- match();
+ /** {@inheritDoc} */
+ @Override
+ public void removeObserver(@NonNull Observer<State> observer) {
+ mEncoderExecutor.execute(
+ () -> mStateObservers.remove(Preconditions.checkNotNull(observer)));
+ }
+
+ @ExecutedBy("mEncoderExecutor")
+ void setActive(boolean isActive) {
+ final State newState = isActive ? State.ACTIVE : State.INACTIVE;
+ if (mBufferProviderState == newState) {
+ return;
}
- }
+ mBufferProviderState = newState;
- @GuardedBy("mLock")
- private void match() {
- if (!mListenerQueue.isEmpty() && !mFreeBufferIndexQueue.isEmpty()) {
- Consumer<Integer> listener = mListenerQueue.poll();
- Integer index = mFreeBufferIndexQueue.poll();
+ if (newState == State.INACTIVE) {
+ for (ListenableFuture<InputBuffer> future : mAcquisitionList) {
+ future.cancel(true);
+ }
+ mAcquisitionList.clear();
+ }
+
+ for (Map.Entry<Observer<State>, Executor> entry : mStateObservers.entrySet()) {
try {
- mExecutor.execute(() -> listener.accept(index));
+ entry.getValue().execute(() -> entry.getKey().onNewData(newState));
} catch (RejectedExecutionException e) {
Logger.e(TAG, "Unable to post to the supplied executor.", e);
- putFreeBufferIndex(index);
}
}
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/InputBuffer.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/InputBuffer.java
new file mode 100644
index 0000000..fba76c7
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/InputBuffer.java
@@ -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.camera.video.internal.encoder;
+
+import androidx.annotation.NonNull;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.nio.ByteBuffer;
+
+/**
+ * InputBuffer is provided by the {@link Encoder} and used to feed data into the {@link Encoder}.
+ *
+ * <p>Once {@link InputBuffer} is complete or no longer needed, {@link #submit} or
+ * {@link #cancel} must be called to return the request to the encoder, otherwise, it will cause
+ * leakage or failure.
+ */
+public interface InputBuffer {
+
+ /**
+ * Gets the {@link ByteBuffer} of the input buffer.
+ *
+ * <p>Before submitting the InputBuffer, the internal position of the ByteBuffer must be set
+ * to prepare it for reading, e.g. the {@link ByteBuffer#position} is the beginning of the
+ * data, usually 0; the {@link ByteBuffer#limit} is the end of the data. Usually
+ * {@link ByteBuffer#flip} is used after writing data.
+ *
+ * <p>Getting ByteBuffer multiple times won't reset its internal position and data.
+ *
+ * @throws {@link IllegalStateException} if InputBuffer is submitted or canceled.
+ */
+ @NonNull
+ ByteBuffer getByteBuffer();
+
+ /**
+ * Sets the timestamp of the input buffer in microseconds.
+ *
+ * @throws {@link IllegalStateException} if InputBuffer is submitted or canceled.
+ */
+ void setPresentationTimeUs(long presentationTimeUs);
+
+ /**
+ * Denotes the input buffer is the end of the data stream.
+ *
+ * @throws {@link IllegalStateException} if InputBuffer is submitted or canceled.
+ */
+ void setEndOfStream(boolean isEndOfStream);
+
+ /**
+ * Submits the input buffer.
+ *
+ * <p>The data will be written to encoder only when {@link #submit} is called.
+ *
+ * @return {@code true} if submit successfully; {@code false} if already submitted, failed or
+ * has been canceled.
+ */
+ boolean submit();
+
+ /**
+ * Returns the request to encoder without taking any effect.
+ *
+ * @return {@code true} if cancel successfully; {@code false} if already submitted, failed or
+ * has been canceled.
+ */
+ boolean cancel();
+
+ /**
+ * The {@link ListenableFuture} that is complete when {@link #submit} or {@link #cancel} is
+ * called.
+ */
+ @NonNull
+ ListenableFuture<Void> getTerminationFuture();
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/InputBufferImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/InputBufferImpl.java
new file mode 100644
index 0000000..3f88d73
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/InputBufferImpl.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.encoder;
+
+import android.media.MediaCodec;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+class InputBufferImpl implements InputBuffer {
+ private final MediaCodec mMediaCodec;
+ private final int mBufferIndex;
+ private final ByteBuffer mByteBuffer;
+ private final ListenableFuture<Void> mTerminationFuture;
+ private final CallbackToFutureAdapter.Completer<Void> mTerminationCompleter;
+ private final AtomicBoolean mTerminated = new AtomicBoolean(false);
+ private long mPresentationTimeUs = 0L;
+ private boolean mIsEndOfStream = false;
+
+ InputBufferImpl(@NonNull MediaCodec mediaCodec, @IntRange(from = 0) int bufferIndex)
+ throws MediaCodec.CodecException {
+ mMediaCodec = Preconditions.checkNotNull(mediaCodec);
+ mBufferIndex = Preconditions.checkArgumentNonnegative(bufferIndex);
+ mByteBuffer = mediaCodec.getInputBuffer(bufferIndex);
+ AtomicReference<Completer<Void>> ref = new AtomicReference<>();
+ mTerminationFuture = CallbackToFutureAdapter.getFuture(
+ completer -> {
+ ref.set(completer);
+ return "Terminate InputBuffer";
+ });
+ mTerminationCompleter = Preconditions.checkNotNull(ref.get());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @NonNull
+ public ByteBuffer getByteBuffer() {
+ throwIfTerminated();
+ return mByteBuffer;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setPresentationTimeUs(long presentationTimeUs) {
+ throwIfTerminated();
+ Preconditions.checkArgument(presentationTimeUs >= 0L);
+ mPresentationTimeUs = presentationTimeUs;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setEndOfStream(boolean endOfStream) {
+ throwIfTerminated();
+ mIsEndOfStream = endOfStream;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean submit() {
+ if (mTerminated.getAndSet(true)) {
+ return false;
+ }
+ try {
+ mMediaCodec.queueInputBuffer(mBufferIndex,
+ mByteBuffer.position(),
+ mByteBuffer.limit(),
+ mPresentationTimeUs,
+ mIsEndOfStream ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
+ mTerminationCompleter.set(null);
+ return true;
+ } catch (IllegalStateException e) {
+ mTerminationCompleter.setException(e);
+ return false;
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean cancel() {
+ if (mTerminated.getAndSet(true)) {
+ return false;
+ }
+ try {
+ mMediaCodec.queueInputBuffer(mBufferIndex, 0, 0, 0, 0);
+ mTerminationCompleter.set(null);
+ } catch (IllegalStateException e) {
+ mTerminationCompleter.setException(e);
+ }
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @NonNull
+ public ListenableFuture<Void> getTerminationFuture() {
+ return Futures.nonCancellationPropagating(mTerminationFuture);
+ }
+
+ private void throwIfTerminated() {
+ if (mTerminated.get()) {
+ throw new IllegalStateException("The buffer is submitted or canceled.");
+ }
+ }
+}
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index 60456c9..57591a5 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -1,6 +1,12 @@
// Signature format: 4.0
package androidx.car.app {
+ public final class AppInfo {
+ method public int getLatestCarAppApiLevel();
+ method public String getLibraryVersion();
+ method public int getMinCarAppApiLevel();
+ }
+
public class AppManager {
method public void invalidate();
method public void setSurfaceListener(androidx.car.app.SurfaceListener?);
@@ -14,38 +20,22 @@
field public static final String NAVIGATION_TEMPLATES = "androidx.car.app.NAVIGATION_TEMPLATES";
}
- public abstract class CarAppService extends android.app.Service implements androidx.lifecycle.LifecycleOwner {
+ public abstract class CarAppService extends android.app.Service {
ctor public CarAppService();
- method @CallSuper public void dump(java.io.FileDescriptor, java.io.PrintWriter, String![]?);
- method public void finish();
- method public final androidx.car.app.CarContext getCarContext();
- method public androidx.car.app.HostInfo? getHostInfo();
- method public androidx.lifecycle.Lifecycle getLifecycle();
- method @CallSuper public android.os.IBinder? onBind(android.content.Intent);
- method public void onCarAppFinished();
+ method @CallSuper public final void dump(java.io.FileDescriptor, java.io.PrintWriter, String![]?);
+ method public final androidx.car.app.Session? getCurrentSession();
+ method public final androidx.car.app.HostInfo? getHostInfo();
+ method @CallSuper public final android.os.IBinder? onBind(android.content.Intent);
method public void onCarConfigurationChanged(android.content.res.Configuration);
- method public abstract androidx.car.app.Screen onCreateScreen(android.content.Intent);
- method public final void onDestroy();
+ method public abstract androidx.car.app.Session onCreateSession();
method public void onNewIntent(android.content.Intent);
method public final boolean onUnbind(android.content.Intent);
field public static final String SERVICE_INTERFACE = "androidx.car.app.CarAppService";
}
- public class CarAppVersion {
- method public boolean isGreaterOrEqualTo(androidx.car.app.CarAppVersion);
- method public static androidx.car.app.CarAppVersion? of(String) throws androidx.car.app.MalformedVersionException;
- field public static final androidx.car.app.CarAppVersion HANDSHAKE_MIN_VERSION;
- field public static final androidx.car.app.CarAppVersion INSTANCE;
- }
-
- public enum CarAppVersion.ReleaseSuffix {
- method public static androidx.car.app.CarAppVersion.ReleaseSuffix fromString(String);
- enum_constant public static final androidx.car.app.CarAppVersion.ReleaseSuffix RELEASE_SUFFIX_BETA;
- enum_constant public static final androidx.car.app.CarAppVersion.ReleaseSuffix RELEASE_SUFFIX_EAP;
- }
-
public class CarContext extends android.content.ContextWrapper {
method public void finishCarApp();
+ method public int getCarAppApiLevel();
method public Object getCarService(String);
method public <T> T getCarService(Class<T!>);
method public String getCarServiceName(Class<?>);
@@ -54,11 +44,11 @@
method public void startCarApp(android.content.Intent);
method public static void startCarApp(android.content.Intent, android.content.Intent);
field public static final String ACTION_NAVIGATE = "androidx.car.app.action.NAVIGATE";
- field public static final String APP_SERVICE = "app_manager";
+ field public static final String APP_SERVICE = "app";
field public static final String CAR_SERVICE = "car";
- field public static final String NAVIGATION_SERVICE = "navigation_manager";
- field public static final String SCREEN_MANAGER_SERVICE = "screen_manager";
- field public static final String START_CAR_APP_BINDER_KEY = "StartCarAppBinderKey";
+ field public static final String EXTRA_START_CAR_APP_BINDER_KEY = "androidx.car.app.extra.START_CAR_APP_BINDER_KEY";
+ field public static final String NAVIGATION_SERVICE = "navigation";
+ field public static final String SCREEN_SERVICE = "screen";
}
public final class CarToast {
@@ -90,38 +80,19 @@
}
public class HostInfo {
- ctor public HostInfo(String, int);
method public String getPackageName();
method public int getUid();
}
- public class MalformedVersionException extends java.lang.Exception {
- ctor public MalformedVersionException(String?);
- ctor public MalformedVersionException(String, Throwable);
- ctor public MalformedVersionException(Throwable?);
- }
-
- public interface OnCheckedChangeListenerWrapper {
- method public void onCheckedChange(boolean, androidx.car.app.OnDoneCallback);
- }
-
public interface OnDoneCallback {
method public void onFailure(androidx.car.app.serialization.Bundleable);
method public void onSuccess(androidx.car.app.serialization.Bundleable?);
}
- public interface OnItemVisibilityChangedListenerWrapper {
- method public void onItemVisibilityChanged(int, int, androidx.car.app.OnDoneCallback);
- }
-
public interface OnScreenResultCallback {
method public void onScreenResult(Object?);
}
- public interface OnSelectedListenerWrapper {
- method public void onSelected(int, androidx.car.app.OnDoneCallback);
- }
-
public abstract class Screen implements androidx.lifecycle.LifecycleOwner {
ctor protected Screen(androidx.car.app.CarContext);
method public final void finish();
@@ -145,14 +116,11 @@
method public void remove(androidx.car.app.Screen);
}
- public interface SearchListener {
- method public void onSearchSubmitted(String);
- method public void onSearchTextChanged(String);
- }
-
- public interface SearchListenerWrapper {
- method public void onSearchSubmitted(String, androidx.car.app.OnDoneCallback);
- method public void onSearchTextChanged(String, androidx.car.app.OnDoneCallback);
+ public abstract class Session implements androidx.lifecycle.LifecycleOwner {
+ ctor public Session();
+ method public final androidx.car.app.CarContext getCarContext();
+ method public androidx.lifecycle.Lifecycle getLifecycle();
+ method public abstract androidx.car.app.Screen onCreateScreen(android.content.Intent);
}
public class SurfaceContainer {
@@ -176,6 +144,14 @@
}
+package androidx.car.app.annotations {
+
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public @interface RequiresCarApi {
+ method public abstract int value();
+ }
+
+}
+
package androidx.car.app.model {
public final class Action {
@@ -345,7 +321,7 @@
method public int getImageType();
method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
method public androidx.car.app.model.CarText? getText();
- method public androidx.car.app.model.CarText? getTitle();
+ method public androidx.car.app.model.CarText getTitle();
method public androidx.car.app.model.Toggle? getToggle();
field public static final int IMAGE_TYPE_ICON = 1; // 0x1
field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
@@ -357,7 +333,7 @@
method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, int);
method public androidx.car.app.model.GridItem.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
method public androidx.car.app.model.GridItem.Builder setText(CharSequence?);
- method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence?);
+ method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence);
method public androidx.car.app.model.GridItem.Builder setToggle(androidx.car.app.model.Toggle?);
}
@@ -389,8 +365,8 @@
method public static androidx.car.app.model.ItemList.Builder builder();
method public java.util.List<java.lang.Object!> getItems();
method public androidx.car.app.model.CarText? getNoItemsMessage();
- method public androidx.car.app.OnItemVisibilityChangedListenerWrapper? getOnItemsVisibilityChangedListener();
- method public androidx.car.app.OnSelectedListenerWrapper? getOnSelectedListener();
+ method public androidx.car.app.model.OnItemVisibilityChangedListenerWrapper? getOnItemsVisibilityChangedListener();
+ method public androidx.car.app.model.OnSelectedListenerWrapper? getOnSelectedListener();
method public int getSelectedIndex();
}
@@ -442,7 +418,7 @@
public final class MessageTemplate implements androidx.car.app.model.Template {
method public static androidx.car.app.model.MessageTemplate.Builder builder(CharSequence);
- method public androidx.car.app.model.ActionList? getActionList();
+ method public androidx.car.app.model.ActionList? getActions();
method public androidx.car.app.model.CarText? getDebugMessage();
method public androidx.car.app.model.Action? getHeaderAction();
method public androidx.car.app.model.CarIcon? getIcon();
@@ -474,6 +450,10 @@
method public androidx.car.app.model.Metadata.Builder setPlace(androidx.car.app.model.Place?);
}
+ public interface OnCheckedChangeListenerWrapper {
+ method public void onCheckedChange(boolean, androidx.car.app.OnDoneCallback);
+ }
+
public interface OnClickListener {
method public void onClick();
}
@@ -483,9 +463,17 @@
method public void onClick(androidx.car.app.OnDoneCallback);
}
+ public interface OnItemVisibilityChangedListenerWrapper {
+ method public void onItemVisibilityChanged(int, int, androidx.car.app.OnDoneCallback);
+ }
+
+ public interface OnSelectedListenerWrapper {
+ method public void onSelected(int, androidx.car.app.OnDoneCallback);
+ }
+
public final class Pane {
method public static androidx.car.app.model.Pane.Builder builder();
- method public androidx.car.app.model.ActionList? getActionList();
+ method public androidx.car.app.model.ActionList? getActions();
method public java.util.List<java.lang.Object!> getRows();
method public boolean isLoading();
}
@@ -603,14 +591,19 @@
method public androidx.car.app.model.Row.Builder setToggle(androidx.car.app.model.Toggle?);
}
+ public interface SearchListenerWrapper {
+ method public void onSearchSubmitted(String, androidx.car.app.OnDoneCallback);
+ method public void onSearchTextChanged(String, androidx.car.app.OnDoneCallback);
+ }
+
public final class SearchTemplate implements androidx.car.app.model.Template {
- method public static androidx.car.app.model.SearchTemplate.Builder builder(androidx.car.app.SearchListener);
+ method public static androidx.car.app.model.SearchTemplate.Builder builder(androidx.car.app.model.SearchTemplate.SearchListener);
method public androidx.car.app.model.ActionStrip? getActionStrip();
method public androidx.car.app.model.Action? getHeaderAction();
method public String? getInitialSearchText();
method public androidx.car.app.model.ItemList? getItemList();
method public String? getSearchHint();
- method public androidx.car.app.SearchListenerWrapper getSearchListener();
+ method public androidx.car.app.model.SearchListenerWrapper getSearchListener();
method public boolean isLoading();
method public boolean isShowKeyboardByDefault();
}
@@ -626,6 +619,11 @@
method public androidx.car.app.model.SearchTemplate.Builder setShowKeyboardByDefault(boolean);
}
+ public static interface SearchTemplate.SearchListener {
+ method public void onSearchSubmitted(String);
+ method public void onSearchTextChanged(String);
+ }
+
public class SectionedItemList {
method public static androidx.car.app.model.SectionedItemList create(androidx.car.app.model.ItemList, androidx.car.app.model.CarText);
method public androidx.car.app.model.CarText getHeader();
@@ -659,7 +657,7 @@
public class Toggle {
method public static androidx.car.app.model.Toggle.Builder builder(androidx.car.app.model.Toggle.OnCheckedChangeListener);
- method public androidx.car.app.OnCheckedChangeListenerWrapper getOnCheckedChangeListener();
+ method public androidx.car.app.model.OnCheckedChangeListenerWrapper getOnCheckedChangeListener();
method public boolean isChecked();
}
@@ -771,15 +769,17 @@
package androidx.car.app.navigation {
public class NavigationManager {
+ method @MainThread public void clearNavigationManagerListener();
method @MainThread public void navigationEnded();
method @MainThread public void navigationStarted();
- method @MainThread public void setListener(androidx.car.app.navigation.NavigationManagerListener?);
+ method @MainThread public void setNavigationManagerListener(androidx.car.app.navigation.NavigationManagerListener);
+ method @MainThread public void setNavigationManagerListener(java.util.concurrent.Executor, androidx.car.app.navigation.NavigationManagerListener);
method @MainThread public void updateTrip(androidx.car.app.navigation.model.Trip);
}
public interface NavigationManagerListener {
method public void onAutoDriveEnabled();
- method public void stopNavigation();
+ method public void onStopNavigation();
}
}
@@ -942,9 +942,9 @@
method public androidx.car.app.navigation.model.PlaceListNavigationTemplate build();
method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
- method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setIsLoading(boolean);
method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
method @VisibleForTesting public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setLoading(boolean);
method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setTitle(CharSequence?);
}
@@ -963,9 +963,9 @@
method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate build();
method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
- method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setIsLoading(boolean);
method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
method @VisibleForTesting public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setLoading(boolean);
method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setNavigateAction(androidx.car.app.model.Action);
method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setTitle(CharSequence?);
}
@@ -982,8 +982,8 @@
public static final class RoutingInfo.Builder {
method public androidx.car.app.navigation.model.RoutingInfo build();
method public androidx.car.app.navigation.model.RoutingInfo.Builder setCurrentStep(androidx.car.app.navigation.model.Step, androidx.car.app.model.Distance);
- method public androidx.car.app.navigation.model.RoutingInfo.Builder setIsLoading(boolean);
method public androidx.car.app.navigation.model.RoutingInfo.Builder setJunctionImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.navigation.model.RoutingInfo.Builder setLoading(boolean);
method public androidx.car.app.navigation.model.RoutingInfo.Builder setNextStep(androidx.car.app.navigation.model.Step?);
}
@@ -1050,7 +1050,7 @@
method public androidx.car.app.navigation.model.Trip.Builder clearStepTravelEstimates();
method public androidx.car.app.navigation.model.Trip.Builder clearSteps();
method public androidx.car.app.navigation.model.Trip.Builder setCurrentRoad(CharSequence?);
- method public androidx.car.app.navigation.model.Trip.Builder setIsLoading(boolean);
+ method public androidx.car.app.navigation.model.Trip.Builder setLoading(boolean);
}
}
@@ -1067,8 +1067,8 @@
method public CharSequence? getContentTitle();
method public android.app.PendingIntent? getDeleteIntent();
method public int getImportance();
- method public android.graphics.Bitmap? getLargeIconBitmap();
- method public int getSmallIconResId();
+ method public android.graphics.Bitmap? getLargeIcon();
+ method @DrawableRes public int getSmallIcon();
method public boolean isExtended();
method public static boolean isExtended(android.app.Notification);
}
@@ -1114,3 +1114,13 @@
}
+package androidx.car.app.versioning {
+
+ public class CarAppApiLevels {
+ field public static final int LATEST = 1; // 0x1
+ field public static final int LEVEL_1 = 1; // 0x1
+ field public static final int OLDEST = 1; // 0x1
+ }
+
+}
+
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index 60456c9..57591a5 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -1,6 +1,12 @@
// Signature format: 4.0
package androidx.car.app {
+ public final class AppInfo {
+ method public int getLatestCarAppApiLevel();
+ method public String getLibraryVersion();
+ method public int getMinCarAppApiLevel();
+ }
+
public class AppManager {
method public void invalidate();
method public void setSurfaceListener(androidx.car.app.SurfaceListener?);
@@ -14,38 +20,22 @@
field public static final String NAVIGATION_TEMPLATES = "androidx.car.app.NAVIGATION_TEMPLATES";
}
- public abstract class CarAppService extends android.app.Service implements androidx.lifecycle.LifecycleOwner {
+ public abstract class CarAppService extends android.app.Service {
ctor public CarAppService();
- method @CallSuper public void dump(java.io.FileDescriptor, java.io.PrintWriter, String![]?);
- method public void finish();
- method public final androidx.car.app.CarContext getCarContext();
- method public androidx.car.app.HostInfo? getHostInfo();
- method public androidx.lifecycle.Lifecycle getLifecycle();
- method @CallSuper public android.os.IBinder? onBind(android.content.Intent);
- method public void onCarAppFinished();
+ method @CallSuper public final void dump(java.io.FileDescriptor, java.io.PrintWriter, String![]?);
+ method public final androidx.car.app.Session? getCurrentSession();
+ method public final androidx.car.app.HostInfo? getHostInfo();
+ method @CallSuper public final android.os.IBinder? onBind(android.content.Intent);
method public void onCarConfigurationChanged(android.content.res.Configuration);
- method public abstract androidx.car.app.Screen onCreateScreen(android.content.Intent);
- method public final void onDestroy();
+ method public abstract androidx.car.app.Session onCreateSession();
method public void onNewIntent(android.content.Intent);
method public final boolean onUnbind(android.content.Intent);
field public static final String SERVICE_INTERFACE = "androidx.car.app.CarAppService";
}
- public class CarAppVersion {
- method public boolean isGreaterOrEqualTo(androidx.car.app.CarAppVersion);
- method public static androidx.car.app.CarAppVersion? of(String) throws androidx.car.app.MalformedVersionException;
- field public static final androidx.car.app.CarAppVersion HANDSHAKE_MIN_VERSION;
- field public static final androidx.car.app.CarAppVersion INSTANCE;
- }
-
- public enum CarAppVersion.ReleaseSuffix {
- method public static androidx.car.app.CarAppVersion.ReleaseSuffix fromString(String);
- enum_constant public static final androidx.car.app.CarAppVersion.ReleaseSuffix RELEASE_SUFFIX_BETA;
- enum_constant public static final androidx.car.app.CarAppVersion.ReleaseSuffix RELEASE_SUFFIX_EAP;
- }
-
public class CarContext extends android.content.ContextWrapper {
method public void finishCarApp();
+ method public int getCarAppApiLevel();
method public Object getCarService(String);
method public <T> T getCarService(Class<T!>);
method public String getCarServiceName(Class<?>);
@@ -54,11 +44,11 @@
method public void startCarApp(android.content.Intent);
method public static void startCarApp(android.content.Intent, android.content.Intent);
field public static final String ACTION_NAVIGATE = "androidx.car.app.action.NAVIGATE";
- field public static final String APP_SERVICE = "app_manager";
+ field public static final String APP_SERVICE = "app";
field public static final String CAR_SERVICE = "car";
- field public static final String NAVIGATION_SERVICE = "navigation_manager";
- field public static final String SCREEN_MANAGER_SERVICE = "screen_manager";
- field public static final String START_CAR_APP_BINDER_KEY = "StartCarAppBinderKey";
+ field public static final String EXTRA_START_CAR_APP_BINDER_KEY = "androidx.car.app.extra.START_CAR_APP_BINDER_KEY";
+ field public static final String NAVIGATION_SERVICE = "navigation";
+ field public static final String SCREEN_SERVICE = "screen";
}
public final class CarToast {
@@ -90,38 +80,19 @@
}
public class HostInfo {
- ctor public HostInfo(String, int);
method public String getPackageName();
method public int getUid();
}
- public class MalformedVersionException extends java.lang.Exception {
- ctor public MalformedVersionException(String?);
- ctor public MalformedVersionException(String, Throwable);
- ctor public MalformedVersionException(Throwable?);
- }
-
- public interface OnCheckedChangeListenerWrapper {
- method public void onCheckedChange(boolean, androidx.car.app.OnDoneCallback);
- }
-
public interface OnDoneCallback {
method public void onFailure(androidx.car.app.serialization.Bundleable);
method public void onSuccess(androidx.car.app.serialization.Bundleable?);
}
- public interface OnItemVisibilityChangedListenerWrapper {
- method public void onItemVisibilityChanged(int, int, androidx.car.app.OnDoneCallback);
- }
-
public interface OnScreenResultCallback {
method public void onScreenResult(Object?);
}
- public interface OnSelectedListenerWrapper {
- method public void onSelected(int, androidx.car.app.OnDoneCallback);
- }
-
public abstract class Screen implements androidx.lifecycle.LifecycleOwner {
ctor protected Screen(androidx.car.app.CarContext);
method public final void finish();
@@ -145,14 +116,11 @@
method public void remove(androidx.car.app.Screen);
}
- public interface SearchListener {
- method public void onSearchSubmitted(String);
- method public void onSearchTextChanged(String);
- }
-
- public interface SearchListenerWrapper {
- method public void onSearchSubmitted(String, androidx.car.app.OnDoneCallback);
- method public void onSearchTextChanged(String, androidx.car.app.OnDoneCallback);
+ public abstract class Session implements androidx.lifecycle.LifecycleOwner {
+ ctor public Session();
+ method public final androidx.car.app.CarContext getCarContext();
+ method public androidx.lifecycle.Lifecycle getLifecycle();
+ method public abstract androidx.car.app.Screen onCreateScreen(android.content.Intent);
}
public class SurfaceContainer {
@@ -176,6 +144,14 @@
}
+package androidx.car.app.annotations {
+
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public @interface RequiresCarApi {
+ method public abstract int value();
+ }
+
+}
+
package androidx.car.app.model {
public final class Action {
@@ -345,7 +321,7 @@
method public int getImageType();
method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
method public androidx.car.app.model.CarText? getText();
- method public androidx.car.app.model.CarText? getTitle();
+ method public androidx.car.app.model.CarText getTitle();
method public androidx.car.app.model.Toggle? getToggle();
field public static final int IMAGE_TYPE_ICON = 1; // 0x1
field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
@@ -357,7 +333,7 @@
method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, int);
method public androidx.car.app.model.GridItem.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
method public androidx.car.app.model.GridItem.Builder setText(CharSequence?);
- method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence?);
+ method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence);
method public androidx.car.app.model.GridItem.Builder setToggle(androidx.car.app.model.Toggle?);
}
@@ -389,8 +365,8 @@
method public static androidx.car.app.model.ItemList.Builder builder();
method public java.util.List<java.lang.Object!> getItems();
method public androidx.car.app.model.CarText? getNoItemsMessage();
- method public androidx.car.app.OnItemVisibilityChangedListenerWrapper? getOnItemsVisibilityChangedListener();
- method public androidx.car.app.OnSelectedListenerWrapper? getOnSelectedListener();
+ method public androidx.car.app.model.OnItemVisibilityChangedListenerWrapper? getOnItemsVisibilityChangedListener();
+ method public androidx.car.app.model.OnSelectedListenerWrapper? getOnSelectedListener();
method public int getSelectedIndex();
}
@@ -442,7 +418,7 @@
public final class MessageTemplate implements androidx.car.app.model.Template {
method public static androidx.car.app.model.MessageTemplate.Builder builder(CharSequence);
- method public androidx.car.app.model.ActionList? getActionList();
+ method public androidx.car.app.model.ActionList? getActions();
method public androidx.car.app.model.CarText? getDebugMessage();
method public androidx.car.app.model.Action? getHeaderAction();
method public androidx.car.app.model.CarIcon? getIcon();
@@ -474,6 +450,10 @@
method public androidx.car.app.model.Metadata.Builder setPlace(androidx.car.app.model.Place?);
}
+ public interface OnCheckedChangeListenerWrapper {
+ method public void onCheckedChange(boolean, androidx.car.app.OnDoneCallback);
+ }
+
public interface OnClickListener {
method public void onClick();
}
@@ -483,9 +463,17 @@
method public void onClick(androidx.car.app.OnDoneCallback);
}
+ public interface OnItemVisibilityChangedListenerWrapper {
+ method public void onItemVisibilityChanged(int, int, androidx.car.app.OnDoneCallback);
+ }
+
+ public interface OnSelectedListenerWrapper {
+ method public void onSelected(int, androidx.car.app.OnDoneCallback);
+ }
+
public final class Pane {
method public static androidx.car.app.model.Pane.Builder builder();
- method public androidx.car.app.model.ActionList? getActionList();
+ method public androidx.car.app.model.ActionList? getActions();
method public java.util.List<java.lang.Object!> getRows();
method public boolean isLoading();
}
@@ -603,14 +591,19 @@
method public androidx.car.app.model.Row.Builder setToggle(androidx.car.app.model.Toggle?);
}
+ public interface SearchListenerWrapper {
+ method public void onSearchSubmitted(String, androidx.car.app.OnDoneCallback);
+ method public void onSearchTextChanged(String, androidx.car.app.OnDoneCallback);
+ }
+
public final class SearchTemplate implements androidx.car.app.model.Template {
- method public static androidx.car.app.model.SearchTemplate.Builder builder(androidx.car.app.SearchListener);
+ method public static androidx.car.app.model.SearchTemplate.Builder builder(androidx.car.app.model.SearchTemplate.SearchListener);
method public androidx.car.app.model.ActionStrip? getActionStrip();
method public androidx.car.app.model.Action? getHeaderAction();
method public String? getInitialSearchText();
method public androidx.car.app.model.ItemList? getItemList();
method public String? getSearchHint();
- method public androidx.car.app.SearchListenerWrapper getSearchListener();
+ method public androidx.car.app.model.SearchListenerWrapper getSearchListener();
method public boolean isLoading();
method public boolean isShowKeyboardByDefault();
}
@@ -626,6 +619,11 @@
method public androidx.car.app.model.SearchTemplate.Builder setShowKeyboardByDefault(boolean);
}
+ public static interface SearchTemplate.SearchListener {
+ method public void onSearchSubmitted(String);
+ method public void onSearchTextChanged(String);
+ }
+
public class SectionedItemList {
method public static androidx.car.app.model.SectionedItemList create(androidx.car.app.model.ItemList, androidx.car.app.model.CarText);
method public androidx.car.app.model.CarText getHeader();
@@ -659,7 +657,7 @@
public class Toggle {
method public static androidx.car.app.model.Toggle.Builder builder(androidx.car.app.model.Toggle.OnCheckedChangeListener);
- method public androidx.car.app.OnCheckedChangeListenerWrapper getOnCheckedChangeListener();
+ method public androidx.car.app.model.OnCheckedChangeListenerWrapper getOnCheckedChangeListener();
method public boolean isChecked();
}
@@ -771,15 +769,17 @@
package androidx.car.app.navigation {
public class NavigationManager {
+ method @MainThread public void clearNavigationManagerListener();
method @MainThread public void navigationEnded();
method @MainThread public void navigationStarted();
- method @MainThread public void setListener(androidx.car.app.navigation.NavigationManagerListener?);
+ method @MainThread public void setNavigationManagerListener(androidx.car.app.navigation.NavigationManagerListener);
+ method @MainThread public void setNavigationManagerListener(java.util.concurrent.Executor, androidx.car.app.navigation.NavigationManagerListener);
method @MainThread public void updateTrip(androidx.car.app.navigation.model.Trip);
}
public interface NavigationManagerListener {
method public void onAutoDriveEnabled();
- method public void stopNavigation();
+ method public void onStopNavigation();
}
}
@@ -942,9 +942,9 @@
method public androidx.car.app.navigation.model.PlaceListNavigationTemplate build();
method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
- method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setIsLoading(boolean);
method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
method @VisibleForTesting public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setLoading(boolean);
method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setTitle(CharSequence?);
}
@@ -963,9 +963,9 @@
method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate build();
method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
- method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setIsLoading(boolean);
method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
method @VisibleForTesting public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setLoading(boolean);
method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setNavigateAction(androidx.car.app.model.Action);
method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setTitle(CharSequence?);
}
@@ -982,8 +982,8 @@
public static final class RoutingInfo.Builder {
method public androidx.car.app.navigation.model.RoutingInfo build();
method public androidx.car.app.navigation.model.RoutingInfo.Builder setCurrentStep(androidx.car.app.navigation.model.Step, androidx.car.app.model.Distance);
- method public androidx.car.app.navigation.model.RoutingInfo.Builder setIsLoading(boolean);
method public androidx.car.app.navigation.model.RoutingInfo.Builder setJunctionImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.navigation.model.RoutingInfo.Builder setLoading(boolean);
method public androidx.car.app.navigation.model.RoutingInfo.Builder setNextStep(androidx.car.app.navigation.model.Step?);
}
@@ -1050,7 +1050,7 @@
method public androidx.car.app.navigation.model.Trip.Builder clearStepTravelEstimates();
method public androidx.car.app.navigation.model.Trip.Builder clearSteps();
method public androidx.car.app.navigation.model.Trip.Builder setCurrentRoad(CharSequence?);
- method public androidx.car.app.navigation.model.Trip.Builder setIsLoading(boolean);
+ method public androidx.car.app.navigation.model.Trip.Builder setLoading(boolean);
}
}
@@ -1067,8 +1067,8 @@
method public CharSequence? getContentTitle();
method public android.app.PendingIntent? getDeleteIntent();
method public int getImportance();
- method public android.graphics.Bitmap? getLargeIconBitmap();
- method public int getSmallIconResId();
+ method public android.graphics.Bitmap? getLargeIcon();
+ method @DrawableRes public int getSmallIcon();
method public boolean isExtended();
method public static boolean isExtended(android.app.Notification);
}
@@ -1114,3 +1114,13 @@
}
+package androidx.car.app.versioning {
+
+ public class CarAppApiLevels {
+ field public static final int LATEST = 1; // 0x1
+ field public static final int LEVEL_1 = 1; // 0x1
+ field public static final int OLDEST = 1; // 0x1
+ }
+
+}
+
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index 60456c9..57591a5 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -1,6 +1,12 @@
// Signature format: 4.0
package androidx.car.app {
+ public final class AppInfo {
+ method public int getLatestCarAppApiLevel();
+ method public String getLibraryVersion();
+ method public int getMinCarAppApiLevel();
+ }
+
public class AppManager {
method public void invalidate();
method public void setSurfaceListener(androidx.car.app.SurfaceListener?);
@@ -14,38 +20,22 @@
field public static final String NAVIGATION_TEMPLATES = "androidx.car.app.NAVIGATION_TEMPLATES";
}
- public abstract class CarAppService extends android.app.Service implements androidx.lifecycle.LifecycleOwner {
+ public abstract class CarAppService extends android.app.Service {
ctor public CarAppService();
- method @CallSuper public void dump(java.io.FileDescriptor, java.io.PrintWriter, String![]?);
- method public void finish();
- method public final androidx.car.app.CarContext getCarContext();
- method public androidx.car.app.HostInfo? getHostInfo();
- method public androidx.lifecycle.Lifecycle getLifecycle();
- method @CallSuper public android.os.IBinder? onBind(android.content.Intent);
- method public void onCarAppFinished();
+ method @CallSuper public final void dump(java.io.FileDescriptor, java.io.PrintWriter, String![]?);
+ method public final androidx.car.app.Session? getCurrentSession();
+ method public final androidx.car.app.HostInfo? getHostInfo();
+ method @CallSuper public final android.os.IBinder? onBind(android.content.Intent);
method public void onCarConfigurationChanged(android.content.res.Configuration);
- method public abstract androidx.car.app.Screen onCreateScreen(android.content.Intent);
- method public final void onDestroy();
+ method public abstract androidx.car.app.Session onCreateSession();
method public void onNewIntent(android.content.Intent);
method public final boolean onUnbind(android.content.Intent);
field public static final String SERVICE_INTERFACE = "androidx.car.app.CarAppService";
}
- public class CarAppVersion {
- method public boolean isGreaterOrEqualTo(androidx.car.app.CarAppVersion);
- method public static androidx.car.app.CarAppVersion? of(String) throws androidx.car.app.MalformedVersionException;
- field public static final androidx.car.app.CarAppVersion HANDSHAKE_MIN_VERSION;
- field public static final androidx.car.app.CarAppVersion INSTANCE;
- }
-
- public enum CarAppVersion.ReleaseSuffix {
- method public static androidx.car.app.CarAppVersion.ReleaseSuffix fromString(String);
- enum_constant public static final androidx.car.app.CarAppVersion.ReleaseSuffix RELEASE_SUFFIX_BETA;
- enum_constant public static final androidx.car.app.CarAppVersion.ReleaseSuffix RELEASE_SUFFIX_EAP;
- }
-
public class CarContext extends android.content.ContextWrapper {
method public void finishCarApp();
+ method public int getCarAppApiLevel();
method public Object getCarService(String);
method public <T> T getCarService(Class<T!>);
method public String getCarServiceName(Class<?>);
@@ -54,11 +44,11 @@
method public void startCarApp(android.content.Intent);
method public static void startCarApp(android.content.Intent, android.content.Intent);
field public static final String ACTION_NAVIGATE = "androidx.car.app.action.NAVIGATE";
- field public static final String APP_SERVICE = "app_manager";
+ field public static final String APP_SERVICE = "app";
field public static final String CAR_SERVICE = "car";
- field public static final String NAVIGATION_SERVICE = "navigation_manager";
- field public static final String SCREEN_MANAGER_SERVICE = "screen_manager";
- field public static final String START_CAR_APP_BINDER_KEY = "StartCarAppBinderKey";
+ field public static final String EXTRA_START_CAR_APP_BINDER_KEY = "androidx.car.app.extra.START_CAR_APP_BINDER_KEY";
+ field public static final String NAVIGATION_SERVICE = "navigation";
+ field public static final String SCREEN_SERVICE = "screen";
}
public final class CarToast {
@@ -90,38 +80,19 @@
}
public class HostInfo {
- ctor public HostInfo(String, int);
method public String getPackageName();
method public int getUid();
}
- public class MalformedVersionException extends java.lang.Exception {
- ctor public MalformedVersionException(String?);
- ctor public MalformedVersionException(String, Throwable);
- ctor public MalformedVersionException(Throwable?);
- }
-
- public interface OnCheckedChangeListenerWrapper {
- method public void onCheckedChange(boolean, androidx.car.app.OnDoneCallback);
- }
-
public interface OnDoneCallback {
method public void onFailure(androidx.car.app.serialization.Bundleable);
method public void onSuccess(androidx.car.app.serialization.Bundleable?);
}
- public interface OnItemVisibilityChangedListenerWrapper {
- method public void onItemVisibilityChanged(int, int, androidx.car.app.OnDoneCallback);
- }
-
public interface OnScreenResultCallback {
method public void onScreenResult(Object?);
}
- public interface OnSelectedListenerWrapper {
- method public void onSelected(int, androidx.car.app.OnDoneCallback);
- }
-
public abstract class Screen implements androidx.lifecycle.LifecycleOwner {
ctor protected Screen(androidx.car.app.CarContext);
method public final void finish();
@@ -145,14 +116,11 @@
method public void remove(androidx.car.app.Screen);
}
- public interface SearchListener {
- method public void onSearchSubmitted(String);
- method public void onSearchTextChanged(String);
- }
-
- public interface SearchListenerWrapper {
- method public void onSearchSubmitted(String, androidx.car.app.OnDoneCallback);
- method public void onSearchTextChanged(String, androidx.car.app.OnDoneCallback);
+ public abstract class Session implements androidx.lifecycle.LifecycleOwner {
+ ctor public Session();
+ method public final androidx.car.app.CarContext getCarContext();
+ method public androidx.lifecycle.Lifecycle getLifecycle();
+ method public abstract androidx.car.app.Screen onCreateScreen(android.content.Intent);
}
public class SurfaceContainer {
@@ -176,6 +144,14 @@
}
+package androidx.car.app.annotations {
+
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD}) public @interface RequiresCarApi {
+ method public abstract int value();
+ }
+
+}
+
package androidx.car.app.model {
public final class Action {
@@ -345,7 +321,7 @@
method public int getImageType();
method public androidx.car.app.model.OnClickListenerWrapper? getOnClickListener();
method public androidx.car.app.model.CarText? getText();
- method public androidx.car.app.model.CarText? getTitle();
+ method public androidx.car.app.model.CarText getTitle();
method public androidx.car.app.model.Toggle? getToggle();
field public static final int IMAGE_TYPE_ICON = 1; // 0x1
field public static final int IMAGE_TYPE_LARGE = 2; // 0x2
@@ -357,7 +333,7 @@
method public androidx.car.app.model.GridItem.Builder setImage(androidx.car.app.model.CarIcon, int);
method public androidx.car.app.model.GridItem.Builder setOnClickListener(androidx.car.app.model.OnClickListener?);
method public androidx.car.app.model.GridItem.Builder setText(CharSequence?);
- method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence?);
+ method public androidx.car.app.model.GridItem.Builder setTitle(CharSequence);
method public androidx.car.app.model.GridItem.Builder setToggle(androidx.car.app.model.Toggle?);
}
@@ -389,8 +365,8 @@
method public static androidx.car.app.model.ItemList.Builder builder();
method public java.util.List<java.lang.Object!> getItems();
method public androidx.car.app.model.CarText? getNoItemsMessage();
- method public androidx.car.app.OnItemVisibilityChangedListenerWrapper? getOnItemsVisibilityChangedListener();
- method public androidx.car.app.OnSelectedListenerWrapper? getOnSelectedListener();
+ method public androidx.car.app.model.OnItemVisibilityChangedListenerWrapper? getOnItemsVisibilityChangedListener();
+ method public androidx.car.app.model.OnSelectedListenerWrapper? getOnSelectedListener();
method public int getSelectedIndex();
}
@@ -442,7 +418,7 @@
public final class MessageTemplate implements androidx.car.app.model.Template {
method public static androidx.car.app.model.MessageTemplate.Builder builder(CharSequence);
- method public androidx.car.app.model.ActionList? getActionList();
+ method public androidx.car.app.model.ActionList? getActions();
method public androidx.car.app.model.CarText? getDebugMessage();
method public androidx.car.app.model.Action? getHeaderAction();
method public androidx.car.app.model.CarIcon? getIcon();
@@ -474,6 +450,10 @@
method public androidx.car.app.model.Metadata.Builder setPlace(androidx.car.app.model.Place?);
}
+ public interface OnCheckedChangeListenerWrapper {
+ method public void onCheckedChange(boolean, androidx.car.app.OnDoneCallback);
+ }
+
public interface OnClickListener {
method public void onClick();
}
@@ -483,9 +463,17 @@
method public void onClick(androidx.car.app.OnDoneCallback);
}
+ public interface OnItemVisibilityChangedListenerWrapper {
+ method public void onItemVisibilityChanged(int, int, androidx.car.app.OnDoneCallback);
+ }
+
+ public interface OnSelectedListenerWrapper {
+ method public void onSelected(int, androidx.car.app.OnDoneCallback);
+ }
+
public final class Pane {
method public static androidx.car.app.model.Pane.Builder builder();
- method public androidx.car.app.model.ActionList? getActionList();
+ method public androidx.car.app.model.ActionList? getActions();
method public java.util.List<java.lang.Object!> getRows();
method public boolean isLoading();
}
@@ -603,14 +591,19 @@
method public androidx.car.app.model.Row.Builder setToggle(androidx.car.app.model.Toggle?);
}
+ public interface SearchListenerWrapper {
+ method public void onSearchSubmitted(String, androidx.car.app.OnDoneCallback);
+ method public void onSearchTextChanged(String, androidx.car.app.OnDoneCallback);
+ }
+
public final class SearchTemplate implements androidx.car.app.model.Template {
- method public static androidx.car.app.model.SearchTemplate.Builder builder(androidx.car.app.SearchListener);
+ method public static androidx.car.app.model.SearchTemplate.Builder builder(androidx.car.app.model.SearchTemplate.SearchListener);
method public androidx.car.app.model.ActionStrip? getActionStrip();
method public androidx.car.app.model.Action? getHeaderAction();
method public String? getInitialSearchText();
method public androidx.car.app.model.ItemList? getItemList();
method public String? getSearchHint();
- method public androidx.car.app.SearchListenerWrapper getSearchListener();
+ method public androidx.car.app.model.SearchListenerWrapper getSearchListener();
method public boolean isLoading();
method public boolean isShowKeyboardByDefault();
}
@@ -626,6 +619,11 @@
method public androidx.car.app.model.SearchTemplate.Builder setShowKeyboardByDefault(boolean);
}
+ public static interface SearchTemplate.SearchListener {
+ method public void onSearchSubmitted(String);
+ method public void onSearchTextChanged(String);
+ }
+
public class SectionedItemList {
method public static androidx.car.app.model.SectionedItemList create(androidx.car.app.model.ItemList, androidx.car.app.model.CarText);
method public androidx.car.app.model.CarText getHeader();
@@ -659,7 +657,7 @@
public class Toggle {
method public static androidx.car.app.model.Toggle.Builder builder(androidx.car.app.model.Toggle.OnCheckedChangeListener);
- method public androidx.car.app.OnCheckedChangeListenerWrapper getOnCheckedChangeListener();
+ method public androidx.car.app.model.OnCheckedChangeListenerWrapper getOnCheckedChangeListener();
method public boolean isChecked();
}
@@ -771,15 +769,17 @@
package androidx.car.app.navigation {
public class NavigationManager {
+ method @MainThread public void clearNavigationManagerListener();
method @MainThread public void navigationEnded();
method @MainThread public void navigationStarted();
- method @MainThread public void setListener(androidx.car.app.navigation.NavigationManagerListener?);
+ method @MainThread public void setNavigationManagerListener(androidx.car.app.navigation.NavigationManagerListener);
+ method @MainThread public void setNavigationManagerListener(java.util.concurrent.Executor, androidx.car.app.navigation.NavigationManagerListener);
method @MainThread public void updateTrip(androidx.car.app.navigation.model.Trip);
}
public interface NavigationManagerListener {
method public void onAutoDriveEnabled();
- method public void stopNavigation();
+ method public void onStopNavigation();
}
}
@@ -942,9 +942,9 @@
method public androidx.car.app.navigation.model.PlaceListNavigationTemplate build();
method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
- method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setIsLoading(boolean);
method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
method @VisibleForTesting public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+ method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setLoading(boolean);
method public androidx.car.app.navigation.model.PlaceListNavigationTemplate.Builder setTitle(CharSequence?);
}
@@ -963,9 +963,9 @@
method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate build();
method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setActionStrip(androidx.car.app.model.ActionStrip?);
method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setHeaderAction(androidx.car.app.model.Action?);
- method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setIsLoading(boolean);
method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemList(androidx.car.app.model.ItemList?);
method @VisibleForTesting public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setItemListForTesting(androidx.car.app.model.ItemList?);
+ method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setLoading(boolean);
method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setNavigateAction(androidx.car.app.model.Action);
method public androidx.car.app.navigation.model.RoutePreviewNavigationTemplate.Builder setTitle(CharSequence?);
}
@@ -982,8 +982,8 @@
public static final class RoutingInfo.Builder {
method public androidx.car.app.navigation.model.RoutingInfo build();
method public androidx.car.app.navigation.model.RoutingInfo.Builder setCurrentStep(androidx.car.app.navigation.model.Step, androidx.car.app.model.Distance);
- method public androidx.car.app.navigation.model.RoutingInfo.Builder setIsLoading(boolean);
method public androidx.car.app.navigation.model.RoutingInfo.Builder setJunctionImage(androidx.car.app.model.CarIcon?);
+ method public androidx.car.app.navigation.model.RoutingInfo.Builder setLoading(boolean);
method public androidx.car.app.navigation.model.RoutingInfo.Builder setNextStep(androidx.car.app.navigation.model.Step?);
}
@@ -1050,7 +1050,7 @@
method public androidx.car.app.navigation.model.Trip.Builder clearStepTravelEstimates();
method public androidx.car.app.navigation.model.Trip.Builder clearSteps();
method public androidx.car.app.navigation.model.Trip.Builder setCurrentRoad(CharSequence?);
- method public androidx.car.app.navigation.model.Trip.Builder setIsLoading(boolean);
+ method public androidx.car.app.navigation.model.Trip.Builder setLoading(boolean);
}
}
@@ -1067,8 +1067,8 @@
method public CharSequence? getContentTitle();
method public android.app.PendingIntent? getDeleteIntent();
method public int getImportance();
- method public android.graphics.Bitmap? getLargeIconBitmap();
- method public int getSmallIconResId();
+ method public android.graphics.Bitmap? getLargeIcon();
+ method @DrawableRes public int getSmallIcon();
method public boolean isExtended();
method public static boolean isExtended(android.app.Notification);
}
@@ -1114,3 +1114,13 @@
}
+package androidx.car.app.versioning {
+
+ public class CarAppApiLevels {
+ field public static final int LATEST = 1; // 0x1
+ field public static final int LEVEL_1 = 1; // 0x1
+ field public static final int OLDEST = 1; // 0x1
+ }
+
+}
+
diff --git a/car/app/app/build.gradle b/car/app/app/build.gradle
index 5e853f9..d0811a6 100644
--- a/car/app/app/build.gradle
+++ b/car/app/app/build.gradle
@@ -31,21 +31,22 @@
implementation "androidx.lifecycle:lifecycle-viewmodel:2.2.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
- androidTestImplementation(ANDROIDX_TEST_CORE)
- androidTestImplementation(ANDROIDX_TEST_RULES)
- androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
- androidTestImplementation(ANDROIDX_TEST_RUNNER)
// TODO(shiufai): We need this for assertThrows. Point back to the AndroidX shared version if
// it is ever upgraded.
- androidTestImplementation("junit:junit:4.13")
- androidTestImplementation(TRUTH)
- androidTestImplementation(MOCKITO_ANDROID)
+ testImplementation("junit:junit:4.13")
+ testImplementation(ANDROIDX_TEST_CORE)
+ testImplementation(ANDROIDX_TEST_RUNNER)
+ testImplementation(JUNIT)
+ testImplementation(MOCKITO_CORE)
+ testImplementation(ROBOLECTRIC)
+ testImplementation(TRUTH)
}
android {
defaultConfig {
minSdkVersion 21
multiDexEnabled = true
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions {
// We have a bunch of builder/inner classes where the outer classes access the private
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/CarAppVersionTest.java b/car/app/app/src/androidTest/java/androidx/car/app/CarAppVersionTest.java
deleted file mode 100644
index b4d28ee..0000000
--- a/car/app/app/src/androidTest/java/androidx/car/app/CarAppVersionTest.java
+++ /dev/null
@@ -1,131 +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.car.app;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-
-import androidx.car.app.CarAppVersion.ReleaseSuffix;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/** Tests for {@link CarAppVersion}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public final class CarAppVersionTest {
-
- @Test
- public void majorVersion() {
- CarAppVersion hostVersion = CarAppVersion.create(2, 0, 0);
- CarAppVersion clientVersion = CarAppVersion.create(1, 0, 0);
-
- assertThat(hostVersion.isGreaterOrEqualTo(clientVersion)).isTrue();
- assertThat(clientVersion.isGreaterOrEqualTo(hostVersion)).isFalse();
- }
-
- @Test
- public void minorVersion() {
- CarAppVersion hostVersion = CarAppVersion.create(2, 1, 0);
- CarAppVersion clientVersion = CarAppVersion.create(2, 0, 0);
-
- assertThat(hostVersion.isGreaterOrEqualTo(clientVersion)).isTrue();
- assertThat(clientVersion.isGreaterOrEqualTo(hostVersion)).isFalse();
- }
-
- @Test
- public void patchVersion() {
- CarAppVersion hostVersion = CarAppVersion.create(3, 2, 1);
- CarAppVersion clientVersion = CarAppVersion.create(3, 2, 0);
-
- assertThat(hostVersion.isGreaterOrEqualTo(clientVersion)).isTrue();
- assertThat(clientVersion.isGreaterOrEqualTo(hostVersion)).isFalse();
- }
-
- @Test
- public void eapVersion_requiresExactMatch() {
- CarAppVersion hostVersion = CarAppVersion.create(3, 2, 1, ReleaseSuffix.RELEASE_SUFFIX_EAP,
- 1);
-
- CarAppVersion mismatchedClientVersion = CarAppVersion.create(4, 3, 2);
- assertThat(hostVersion.isGreaterOrEqualTo(mismatchedClientVersion)).isFalse();
- assertThat(mismatchedClientVersion.isGreaterOrEqualTo(hostVersion)).isFalse();
-
- CarAppVersion matchedClientVersion =
- CarAppVersion.create(3, 2, 1, ReleaseSuffix.RELEASE_SUFFIX_EAP, 1);
- assertThat(hostVersion.isGreaterOrEqualTo(matchedClientVersion)).isTrue();
- assertThat(matchedClientVersion.isGreaterOrEqualTo(hostVersion)).isTrue();
- }
-
- @Test
- public void stableVersion_compatibleWithAllBeta() {
- CarAppVersion hostVersion = CarAppVersion.create(3, 2, 1);
-
- CarAppVersion clientVersion1 =
- CarAppVersion.create(3, 2, 1, ReleaseSuffix.RELEASE_SUFFIX_BETA, 1);
- assertThat(hostVersion.isGreaterOrEqualTo(clientVersion1)).isTrue();
- assertThat(clientVersion1.isGreaterOrEqualTo(hostVersion)).isFalse();
-
- CarAppVersion clientVersion2 =
- CarAppVersion.create(3, 2, 1, ReleaseSuffix.RELEASE_SUFFIX_BETA, 2);
- assertThat(hostVersion.isGreaterOrEqualTo(clientVersion2)).isTrue();
- assertThat(clientVersion2.isGreaterOrEqualTo(hostVersion)).isFalse();
- }
-
- @Test
- public void versionString_malformed_multipleHyphens() {
- assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2.3-eap.4-5"));
- }
-
- @Test
- public void versionString_malformed_mainVersionIncorrectNumbers() {
- assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1"));
- assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1."));
- assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2"));
- assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2."));
- assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2.3.4"));
- }
-
- @Test
- public void versionString_malformed_invalidNumberFormat() {
- assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2.3c-eap.4"));
- assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2.3-eap.4c"));
- }
-
- @Test
- public void versionString_malformed_incorrectReleaseSuffix() {
- assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2.3-"));
- assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2.3-eap"));
- assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2.3-eap."));
- assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2.3-eap.4."));
- assertThrows(MalformedVersionException.class, () -> CarAppVersion.of("1.2.3-eaP.4"));
- }
-
- @Test
- public void versionString() throws MalformedVersionException {
- String version1 = "1.2.3";
- String version2 = "1.2.3-eap.0";
- String version3 = "1.2.3-beta.1";
-
- assertThat(CarAppVersion.of(version1).toString()).isEqualTo(version1);
- assertThat(CarAppVersion.of(version2).toString()).isEqualTo(version2);
- assertThat(CarAppVersion.of(version3).toString()).isEqualTo(version3);
- }
-}
diff --git a/car/app/app/src/main/aidl/androidx/car/app/ICarApp.aidl b/car/app/app/src/main/aidl/androidx/car/app/ICarApp.aidl
index e8f3995..b7e4f99 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/ICarApp.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/ICarApp.aidl
@@ -61,12 +61,12 @@
void getManager(in String type, IOnDoneCallback callback) = 8;
/**
- * Requests the version string of the library used for building the app.
+ * Requests information of the application (min API level, target API level, etc.).
*/
- void getCarAppVersion(IOnDoneCallback callback) = 9;
+ void getAppInfo(IOnDoneCallback callback) = 9;
/**
- * Sends host information to the app.
+ * Sends host information and negotiated API level to the app.
*/
void onHandshakeCompleted(in Bundleable handshakeInfo, IOnDoneCallback callback) = 10;
}
diff --git a/car/app/app/src/main/aidl/androidx/car/app/IOnCheckedChangeListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/model/IOnCheckedChangeListener.aidl
similarity index 95%
rename from car/app/app/src/main/aidl/androidx/car/app/IOnCheckedChangeListener.aidl
rename to car/app/app/src/main/aidl/androidx/car/app/model/IOnCheckedChangeListener.aidl
index 8383e12..46e758ed 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/IOnCheckedChangeListener.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/model/IOnCheckedChangeListener.aidl
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app;
+package androidx.car.app.model;
import androidx.car.app.IOnDoneCallback;
diff --git a/car/app/app/src/main/aidl/androidx/car/app/IOnItemVisibilityChangedListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/model/IOnItemVisibilityChangedListener.aidl
similarity index 96%
rename from car/app/app/src/main/aidl/androidx/car/app/IOnItemVisibilityChangedListener.aidl
rename to car/app/app/src/main/aidl/androidx/car/app/model/IOnItemVisibilityChangedListener.aidl
index ff43f3f..451a2b0 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/IOnItemVisibilityChangedListener.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/model/IOnItemVisibilityChangedListener.aidl
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app;
+package androidx.car.app.model;
import androidx.car.app.IOnDoneCallback;
diff --git a/car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/model/IOnSelectedListener.aidl
similarity index 95%
rename from car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl
rename to car/app/app/src/main/aidl/androidx/car/app/model/IOnSelectedListener.aidl
index 2896a83..2ee9f5f 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/model/IOnSelectedListener.aidl
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app;
+package androidx.car.app.model;
import androidx.car.app.IOnDoneCallback;
diff --git a/car/app/app/src/main/aidl/androidx/car/app/ISearchListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/model/ISearchListener.aidl
similarity index 96%
rename from car/app/app/src/main/aidl/androidx/car/app/ISearchListener.aidl
rename to car/app/app/src/main/aidl/androidx/car/app/model/ISearchListener.aidl
index 9af81e0..990f255 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/ISearchListener.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/model/ISearchListener.aidl
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app;
+package androidx.car.app.model;
import androidx.car.app.IOnDoneCallback;
diff --git a/car/app/app/src/main/aidl/androidx/car/app/navigation/INavigationManager.aidl b/car/app/app/src/main/aidl/androidx/car/app/navigation/INavigationManager.aidl
index 4e16e7e..7a3a01d 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/navigation/INavigationManager.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/navigation/INavigationManager.aidl
@@ -28,5 +28,5 @@
* <p>The app should stop any audio guidance, routing notifications tagged for
* the car, and metadata state updates.
*/
- void stopNavigation(IOnDoneCallback callback) = 1;
+ void onStopNavigation(IOnDoneCallback callback) = 1;
}
diff --git a/car/app/app/src/main/java/androidx/car/app/AppInfo.java b/car/app/app/src/main/java/androidx/car/app/AppInfo.java
new file mode 100644
index 0000000..4c52330
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/AppInfo.java
@@ -0,0 +1,143 @@
+/*
+ * 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.car.app;
+
+import static androidx.car.app.utils.CommonUtils.TAG;
+
+import static java.util.Objects.requireNonNull;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.VisibleForTesting;
+import androidx.car.app.versioning.CarAppApiLevel;
+import androidx.car.app.versioning.CarAppApiLevels;
+
+/**
+ * Container class for information about the app the host is connected to.
+ * <p>
+ * Hosts will use this information to provide the right level of compatibility, based on the
+ * application's minimum and maximum API level and its own set of supported API levels.
+ * <p>
+ * The application minimum API level is defined in the application's manifest using the
+ * following declaration.
+ * <pre>{@code
+ * <manifest ...>
+ * <application ...>
+ * <meta-data
+ * android:name="androidx.car.app.min-api-level"
+ * android:value="2" />
+ * ...
+ * </application>
+ * </manifest>
+ * }</pre>
+ * <p>
+ * @see CarContext#getCarAppApiLevel()
+ */
+public final class AppInfo {
+ // TODO(b/174803562): Automatically update the this version using Gradle
+ private static final String LIBRARY_VERSION = "1.0.0-alpha01";
+
+ /** @hide */
+ @RestrictTo(Scope.LIBRARY)
+ @VisibleForTesting
+ public static final String MIN_API_LEVEL_MANIFEST_KEY = "androidx.car.app.min-api-level";
+
+ @Nullable
+ private final String mLibraryVersion;
+ @CarAppApiLevel
+ private final int mMinCarAppApiLevel;
+ @CarAppApiLevel
+ private final int mLatestCarAppApiLevel;
+
+ /** @hide */
+ @RestrictTo(Scope.LIBRARY)
+ @NonNull
+ public static AppInfo create(@NonNull Context context) {
+ @CarAppApiLevel
+ int minApiLevel = retrieveMinCarAppApiLevel(context);
+ if (minApiLevel < CarAppApiLevels.OLDEST || minApiLevel > CarAppApiLevels.LATEST) {
+ throw new IllegalArgumentException("Min API level (" + MIN_API_LEVEL_MANIFEST_KEY
+ + "=" + minApiLevel + ") is out of range (" + CarAppApiLevels.OLDEST + "-"
+ + CarAppApiLevels.LATEST + ")");
+ }
+ return new AppInfo(minApiLevel, CarAppApiLevels.LATEST, LIBRARY_VERSION);
+ }
+
+ // Used for serialization
+ /** @hide */
+ @RestrictTo(Scope.LIBRARY)
+ public AppInfo() {
+ mMinCarAppApiLevel = 0;
+ mLibraryVersion = null;
+ mLatestCarAppApiLevel = 0;
+ }
+
+ // Used for testing
+ /** @hide */
+ @RestrictTo(Scope.LIBRARY)
+ @VisibleForTesting
+ public AppInfo(@CarAppApiLevel int minCarAppApiLevel, @CarAppApiLevel int latestCarAppApiLevel,
+ @NonNull String libraryVersion) {
+ mMinCarAppApiLevel = minCarAppApiLevel;
+ mLibraryVersion = libraryVersion;
+ mLatestCarAppApiLevel = latestCarAppApiLevel;
+ }
+
+ /** @hide */
+ @RestrictTo(Scope.LIBRARY)
+ @VisibleForTesting
+ @CarAppApiLevel
+ public static int retrieveMinCarAppApiLevel(@NonNull Context context) {
+ try {
+ ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(
+ context.getPackageName(),
+ PackageManager.GET_META_DATA);
+ if (applicationInfo.metaData == null) {
+ Log.i(TAG, "Min API level not found (" + MIN_API_LEVEL_MANIFEST_KEY + "). "
+ + "Assuming min API level = " + CarAppApiLevels.LATEST);
+ return CarAppApiLevels.LATEST;
+ }
+ return applicationInfo.metaData.getInt(MIN_API_LEVEL_MANIFEST_KEY);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Unable to read min API level from manifest. Assuming "
+ + CarAppApiLevels.LATEST, e);
+ return CarAppApiLevels.LATEST;
+ }
+ }
+
+ @NonNull
+ public String getLibraryVersion() {
+ return requireNonNull(mLibraryVersion);
+ }
+
+ @CarAppApiLevel
+ public int getMinCarAppApiLevel() {
+ return mMinCarAppApiLevel;
+ }
+
+ @CarAppApiLevel
+ public int getLatestCarAppApiLevel() {
+ return mLatestCarAppApiLevel;
+ }
+}
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 7c3c854..a7f71d5 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
@@ -38,7 +38,6 @@
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.Lifecycle.Event;
import androidx.lifecycle.Lifecycle.State;
-import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;
import java.io.FileDescriptor;
@@ -78,39 +77,34 @@
* service</a>. Also note that accessing location may become unreliable when the phone is in the
* battery saver mode.
*/
-// This lint warning is triggered because this has a finish() API. Suppress because we are not
-// actually cleaning any held resources in that method.
-@SuppressWarnings("NotCloseable")
-public abstract class CarAppService extends Service implements LifecycleOwner {
+public abstract class CarAppService extends Service {
/**
* The {@link Intent} that must be declared as handled by the service.
*/
public static final String SERVICE_INTERFACE = "androidx.car.app.CarAppService";
-
private static final String TAG = "CarAppService";
private static final String AUTO_DRIVE = "AUTO_DRIVE";
- @SuppressWarnings({"argument.type.incompatible", "assignment.type.incompatible"})
- private final LifecycleRegistry mRegistry = new LifecycleRegistry(this);
-
- @SuppressWarnings("argument.type.incompatible")
- private final CarContext mCarContext = CarContext.create(mRegistry);
+ @Nullable
+ private Session mCurrentSession;
@Nullable
- HostInfo mHostInfo;
+ private HostInfo mHostInfo;
+ @Nullable
+ private AppInfo mAppInfo;
/**
* Handles the host binding to this car app.
*
* <p>This method is final to ensure this car app's lifecycle is handled properly.
*
- * <p>Use {@link #onCreateScreen} and {@link #onNewIntent} instead to handle incoming {@link
+ * <p>Use {@link #onCreateSession()} and {@link #onNewIntent} instead to handle incoming {@link
* Intent}s.
*/
@Override
@CallSuper
@Nullable
- public IBinder onBind(@NonNull Intent intent) {
+ public final IBinder onBind(@NonNull Intent intent) {
return mBinder;
}
@@ -118,28 +112,28 @@
* Handles the host unbinding from this car app.
*
* <p>This method is final to ensure this car app's lifecycle is handled properly.
- *
- * <p>Use {@link #onCarAppFinished} instead.
*/
@Override
public final boolean onUnbind(@NonNull Intent intent) {
Log.d(TAG, "onUnbind intent: " + intent);
runOnMain(() -> {
- // Stop the car app
- mRegistry.handleLifecycleEvent(Event.ON_STOP);
+ // Destroy the session
+ if (mCurrentSession != null) {
+ CarContext carContext = mCurrentSession.getCarContext();
- // Stop any active navigation
- mCarContext.getCarService(NavigationManager.class).stopNavigation();
+ // Stop any active navigation
+ carContext.getCarService(NavigationManager.class).onStopNavigation();
- // Destroy all screens in the stack
- mCarContext.getCarService(ScreenManager.class).destroyAndClearScreenStack();
+ // Destroy all screens in the stack
+ carContext.getCarService(ScreenManager.class).destroyAndClearScreenStack();
- // Remove binders to the host
- mCarContext.resetHosts();
+ // Remove binders to the host
+ carContext.resetHosts();
- // Notify the app that the host has unbinded so that it may treat it similar
- // to destroy
- onCarAppFinished();
+ ((LifecycleRegistry) mCurrentSession.getLifecycle()).handleLifecycleEvent(
+ Event.ON_DESTROY);
+ }
+ mCurrentSession = null;
});
// Return true to request an onRebind call. This means that the process will cache this
@@ -149,94 +143,21 @@
}
/**
- * Handles the system destroying this {@link CarAppService}.
+ * Creates a new {@link Session} for the application.
*
- * <p>This method is final to ensure this car app's lifecycle is handled properly.
+ * <p>This method is invoked the first time the app is started, or if the previous
+ * {@link Session} instance has been destroyed and the system has not yet destroyed
+ * this service.
*
- * <p>Use a {@link androidx.lifecycle.LifecycleObserver} to observe this car app's {@link
- * Lifecycle}.
- *
- * @see #getLifecycle
- */
- @Override
- public final void onDestroy() {
- Log.d(TAG, "onDestroy");
- runOnMain(
- () -> {
- mRegistry.handleLifecycleEvent(Event.ON_DESTROY);
- });
- super.onDestroy();
- Log.d(TAG, "onDestroy completed");
- }
-
- /**
- * Notifies that this car app has finished and should be treated as if it is destroyed.
- *
- * <p>The {@link Screen}s in the stack managed by the {@link ScreenManager} are now all
- * destroyed and removed from the screen stack.
- *
- * <p>{@link #onCreateScreen} will be called if the user reopens the app before the system has
- * destroyed it.
- *
- * <p>For the purposes of the app's lifecycle, you should perform similar destroy functions that
- * you would when this instance's {@link Lifecycle} becomes {@link Lifecycle.State#DESTROYED}.
+ * <p>Once the method returns, {@link Session#onCreateScreen(Intent)} will be called on the
+ * {@link Session} returned.
*
* <p>Called by the system, do not call this method directly.
*
- * @see #getLifecycle
- */
- public void onCarAppFinished() {
- }
-
- /**
- * Requests to finish the car app.
- *
- * <p>Call this when your app is done and should be closed.
- *
- * <p>At some point after this call {@link #onCarAppFinished} will be called, and eventually the
- * system will destroy this {@link CarAppService}.
- */
- public void finish() {
- mCarContext.finishCarApp();
- }
-
- /**
- * Returns the {@link CarContext} for this car app.
- *
- * <p><b>The {@link CarContext} is not fully initialized until this car app's {@link
- * Lifecycle.State} is at least {@link Lifecycle.State#CREATED}</b>
- *
- * @see #getLifecycle
- */
- @NonNull
- public final CarContext getCarContext() {
- return mCarContext;
- }
-
- /**
- * Requests the first {@link Screen} for the application.
- *
- * <p>This method is invoked when this car app is first opened by the user.
- *
- * <p>Once the method returns, {@link Screen#onGetTemplate} will be called on the {@link Screen}
- * returned, and the app will be displayed on the car screen.
- *
- * <p>To pre-seed a back stack, you can push {@link Screen}s onto the stack, via {@link
- * ScreenManager#push} during this method call.
- *
- * <p>This method is invoked the first time the app is started, or if this {@link CarAppService}
- * instance has been previously finished and the system has not yet destroyed this car app (See
- * {@link #onCarAppFinished}).
- *
- * <p>Called by the system, do not call this method directly.
- *
- * @param intent the intent that was used to start this app. If the app was started with a
- * call to {@link CarContext#startCarApp}, this intent will be equal to the
- * intent passed to that method.
* @see CarContext#startCarApp
*/
@NonNull
- public abstract Screen onCreateScreen(@NonNull Intent intent);
+ public abstract Session onCreateSession();
/**
* Notifies that the car app has received a new {@link Intent}.
@@ -245,7 +166,7 @@
* that is on top of the {@link Screen} stack managed by the {@link ScreenManager}, and the app
* will be displayed on the car screen.
*
- * <p>In contrast to {@link #onCreateScreen}, this method is invoked when the app has already
+ * <p>In contrast to {@link #onCreateSession}, this method is invoked when the app has already
* been launched and has not been finished.
*
* <p>Often used to update the current {@link Screen} or pushing a new one on the stack,
@@ -274,69 +195,21 @@
public void onCarConfigurationChanged(@NonNull Configuration newConfiguration) {
}
- /**
- * Returns the {@link CarAppService}'s {@link Lifecycle}.
- *
- * <p>Here are some of the ways you can use the car app's {@link Lifecycle}:
- *
- * <ul>
- * <li>Observe its {@link Lifecycle} by calling {@link Lifecycle#addObserver}. You can use the
- * {@link androidx.lifecycle.LifecycleObserver} to take specific actions whenever the
- * {@link Screen} receives different {@link Event}s.
- * <li>Use this {@link CarAppService} to observe {@link androidx.lifecycle.LiveData}s that
- * may drive the backing data for your application.
- * </ul>
- *
- * <p>What each lifecycle related event means for a car app:
- *
- * <dl>
- * <dt>{@link Event#ON_CREATE}
- * <dd>The car app has just been launched, and this car app is being initialized. {@link
- * #onCreateScreen} will be called at a point after this call.
- * <dt>{@link #onCreateScreen}
- * <dd>The host is ready for this car app to create the first {@link Screen} so that it can
- * display its template.
- * <dt>{@link Event#ON_START}
- * <dd>The application is now visible in the car screen.
- * <dt>{@link Event#ON_RESUME}
- * <dd>The user can now interact with this application.
- * <dt>{@link Event#ON_PAUSE}
- * <dd>The user can no longer interact with this application.
- * <dt>{@link Event#ON_STOP}
- * <dd>The application is no longer visible.
- * <dt>{@link #onCarAppFinished}
- * <dd>Either this car app has requested to be finished (see {@link #finish}), or the host has
- * finished this car app. Unless this is a navigation app, after a period of time that the
- * app is no longer displaying in the car, the host may finish this car app.
- * <dt>{@link Event#ON_DESTROY}
- * <dd>The OS has now destroyed this {@link CarAppService} instance, and it is no longer
- * valid.
- * </dl>
- *
- * <p>Listeners that are added in {@link Event#ON_START}, should be removed in {@link
- * Event#ON_STOP}.
- *
- * <p>Listeners that are added in {@link Event#ON_CREATE} should be removed in {@link
- * Event#ON_DESTROY}.
- *
- * @see androidx.lifecycle.LifecycleObserver
- */
- @NonNull
- @Override
- public Lifecycle getLifecycle() {
- return mRegistry;
- }
-
@Override
@CallSuper
- public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer,
+ public final void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer,
@Nullable String[] args) {
super.dump(fd, writer, args);
for (String arg : args) {
if (AUTO_DRIVE.equals(arg)) {
Log.d(TAG, "Executing onAutoDriveEnabled");
- runOnMain(mCarContext.getCarService(NavigationManager.class)::onAutoDriveEnabled);
+ runOnMain(() -> {
+ if (mCurrentSession != null) {
+ mCurrentSession.getCarContext().getCarService(
+ NavigationManager.class).onAutoDriveEnabled();
+ }
+ });
}
}
}
@@ -347,10 +220,20 @@
* @see HostInfo
*/
@Nullable
- public HostInfo getHostInfo() {
+ public final HostInfo getHostInfo() {
return mHostInfo;
}
+ /**
+ * Retrieves the current {@link Session} for this service.
+ *
+ * @see Session
+ */
+ @Nullable
+ public final Session getCurrentSession() {
+ return mCurrentSession;
+ }
+
private final ICarApp.Stub mBinder =
new ICarApp.Stub() {
// incompatible argument for parameter context of attachBaseContext.
@@ -367,17 +250,25 @@
IOnDoneCallback callback) {
Log.d(TAG, "onAppCreate intent: " + intent);
RemoteUtils.dispatchHostCall(() -> {
+ if (mCurrentSession == null
+ || mCurrentSession.getLifecycle().getCurrentState()
+ == State.DESTROYED) {
+ mCurrentSession = onCreateSession();
+ mAppInfo = AppInfo.create(mCurrentSession.getCarContext());
+ }
+
// CarContext is not set up until the base Context is attached. First
// thing we need to do here is attach the base Context, so that any usage of
// it works after this point.
- CarContext carContext = getCarContext();
+ CarContext carContext = mCurrentSession.getCarContext();
carContext.attachBaseContext(CarAppService.this, configuration);
carContext.setCarHost(carHost);
// Whenever the host unbinds, the screens in the stack are destroyed. If
// there is another bind, before the OS has destroyed this Service, then
// the stack will be empty, and we need to treat it as a new instance.
- LifecycleRegistry registry = (LifecycleRegistry) getLifecycle();
+ LifecycleRegistry registry =
+ (LifecycleRegistry) mCurrentSession.getLifecycle();
Lifecycle.State state = registry.getCurrentState();
int screenStackSize = carContext.getCarService(
ScreenManager.class).getScreenStack().size();
@@ -388,7 +279,7 @@
+ ", stack size: " + screenStackSize);
registry.handleLifecycleEvent(Event.ON_CREATE);
carContext.getCarService(ScreenManager.class).push(
- onCreateScreen(intent));
+ mCurrentSession.onCreateScreen(intent));
} else {
Log.d(TAG, "onAppCreate the app was already created");
onNewIntentInternal(intent);
@@ -400,29 +291,43 @@
@Override
public void onAppStart(IOnDoneCallback callback) {
RemoteUtils.dispatchHostCall(
- () -> ((LifecycleRegistry) getLifecycle()).handleLifecycleEvent(
- Event.ON_START), callback, "onAppStart");
+ () -> {
+ checkSessionIsValid(mCurrentSession);
+ ((LifecycleRegistry) mCurrentSession.getLifecycle())
+ .handleLifecycleEvent(Event.ON_START);
+ }, callback,
+ "onAppStart");
}
@Override
public void onAppResume(IOnDoneCallback callback) {
RemoteUtils.dispatchHostCall(
- () -> ((LifecycleRegistry) getLifecycle()).handleLifecycleEvent(
- Event.ON_RESUME), callback, "onAppResume");
+ () -> {
+ checkSessionIsValid(mCurrentSession);
+ ((LifecycleRegistry) mCurrentSession.getLifecycle())
+ .handleLifecycleEvent(Event.ON_RESUME);
+ }, callback,
+ "onAppResume");
}
@Override
public void onAppPause(IOnDoneCallback callback) {
RemoteUtils.dispatchHostCall(
- () -> ((LifecycleRegistry) getLifecycle()).handleLifecycleEvent(
- Event.ON_PAUSE), callback, "onAppPause");
+ () -> {
+ checkSessionIsValid(mCurrentSession);
+ ((LifecycleRegistry) mCurrentSession.getLifecycle())
+ .handleLifecycleEvent(Event.ON_PAUSE);
+ }, callback, "onAppPause");
}
@Override
public void onAppStop(IOnDoneCallback callback) {
RemoteUtils.dispatchHostCall(
- () -> ((LifecycleRegistry) getLifecycle()).handleLifecycleEvent(
- Event.ON_STOP), callback, "onAppStop");
+ () -> {
+ checkSessionIsValid(mCurrentSession);
+ ((LifecycleRegistry) mCurrentSession.getLifecycle())
+ .handleLifecycleEvent(Event.ON_STOP);
+ }, callback, "onAppStop");
}
@Override
@@ -448,14 +353,14 @@
RemoteUtils.sendSuccessResponse(
callback,
"getManager",
- getCarContext().getCarService(
+ mCurrentSession.getCarContext().getCarService(
AppManager.class).getIInterface());
return;
case CarContext.NAVIGATION_SERVICE:
RemoteUtils.sendSuccessResponse(
callback,
"getManager",
- mCarContext.getCarService(
+ mCurrentSession.getCarContext().getCarService(
NavigationManager.class).getIInterface());
return;
default:
@@ -467,9 +372,9 @@
}
@Override
- public void getCarAppVersion(IOnDoneCallback callback) {
+ public void getAppInfo(IOnDoneCallback callback) {
RemoteUtils.sendSuccessResponse(
- callback, "getCarAppVersion", CarAppVersion.INSTANCE.toString());
+ callback, "getAppInfo", mAppInfo);
}
@Override
@@ -481,10 +386,13 @@
String packageName = deserializedHandshakeInfo.getHostPackageName();
int uid = Binder.getCallingUid();
mHostInfo = new HostInfo(packageName, uid);
- } catch (BundlerException e) {
+ mCurrentSession.getCarContext().onHandshakeComplete(
+ deserializedHandshakeInfo);
+ RemoteUtils.sendSuccessResponse(callback, "onHandshakeCompleted", null);
+ } catch (BundlerException | IllegalArgumentException e) {
mHostInfo = null;
+ RemoteUtils.sendFailureResponse(callback, "onHandshakeCompleted", e);
}
- RemoteUtils.sendSuccessResponse(callback, "onHandshakeCompleted", null);
}
// call to onNewIntent(android.content.Intent) not allowed on the given receiver.
@@ -504,8 +412,15 @@
ThreadUtils.checkMainThread();
Log.d(TAG, "onCarConfigurationChanged configuration: " + configuration);
- getCarContext().onCarConfigurationChanged(configuration);
- onCarConfigurationChanged(getCarContext().getResources().getConfiguration());
+ mCurrentSession.getCarContext().onCarConfigurationChanged(configuration);
+ onCarConfigurationChanged(
+ mCurrentSession.getCarContext().getResources().getConfiguration());
}
};
+
+ private static void checkSessionIsValid(Session session) {
+ if (session == null) {
+ throw new IllegalStateException("Null session found when non-null expected.");
+ }
+ }
}
diff --git a/car/app/app/src/main/java/androidx/car/app/CarAppVersion.java b/car/app/app/src/main/java/androidx/car/app/CarAppVersion.java
deleted file mode 100644
index d23f7e1..0000000
--- a/car/app/app/src/main/java/androidx/car/app/CarAppVersion.java
+++ /dev/null
@@ -1,270 +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.car.app;
-
-import static androidx.car.app.CarAppVersion.ReleaseSuffix.RELEASE_SUFFIX_BETA;
-import static androidx.car.app.CarAppVersion.ReleaseSuffix.RELEASE_SUFFIX_EAP;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.Locale;
-
-/**
- * A versioning class used for compatibility checks between the host and client.
- *
- * <p>The version scheme follows semantic versioning and is defined as:
- *
- * <pre>major.minor.patch[-releasesSuffix.build]<pre>
- *
- * where:
- *
- * <ul>
- * <li>major version differences denote binary incompatibility (e.g. API removal)
- * <li>minor version differences denote compatible API changes (e.g. API additional/deprecation)
- * <li>patch version differences denote non-API altering internal changes (e.g. bug fixes)
- * <li>releaseSuffix is{@code null} for stable versions but otherwise reserved for special-purpose
- * EAP builds and one-off public betas. For cases where a release suffix is provided, the
- * appended build number is used to differentiate versions within the same suffix category.
- * (e.g. 1.0.0-eap.1 vs 1.0.0-eap.2).
- * </ul>
- */
-public class CarAppVersion {
- private static final String MAIN_VERSION_FORMAT = "%d.%d.%d";
- private static final String SUFFIX_VERSION_FORMAT = "-%s.%d";
-
- public static final CarAppVersion INSTANCE =
- CarAppVersion.create(1, 0, 0, RELEASE_SUFFIX_BETA, 1);
-
- /** Min version of the SDK which supports handshake completed binder call. */
- public static final CarAppVersion HANDSHAKE_MIN_VERSION =
- CarAppVersion.create(1, 0, 0, RELEASE_SUFFIX_BETA, 1);
-
- /** Different types of supported version suffixes. */
- public enum ReleaseSuffix {
- RELEASE_SUFFIX_EAP("eap"),
- RELEASE_SUFFIX_BETA("beta");
-
- private final String mValue;
-
- ReleaseSuffix(String value) {
- this.mValue = value;
- }
-
- /** Creates a {@link ReleaseSuffix} from the string standard representation as described
- * in the {@link #toString()} method */
- @NonNull
- public static ReleaseSuffix fromString(@NonNull String value) {
- switch (value) {
- case "eap":
- return RELEASE_SUFFIX_EAP;
- case "beta":
- return RELEASE_SUFFIX_BETA;
- default:
- throw new IllegalArgumentException(value + " is not a valid release suffix");
- }
- }
-
- @NonNull
- @Override
- public String toString() {
- return mValue;
- }
- }
-
- private final int mMajor;
- private final int mMinor;
- private final int mPatch;
- @Nullable
- private final ReleaseSuffix mReleaseSuffix;
- private final int mBuild;
-
- /** Creates a {@link CarAppVersion} without a release suffix. (e.g. 1.0.0) */
- @NonNull
- static CarAppVersion create(int major, int minor, int patch) {
- return new CarAppVersion(major, minor, patch, null, 0);
- }
-
- /**
- * Creates a {@link CarAppVersion} with a release suffix and build number. (e.g. 1.0.0-eap.0)
- *
- * <p>Note that if {@code releaseSuffix} is {@code null}, then {@code build} is ignored.
- */
- @NonNull
- static CarAppVersion create(
- int major, int minor, int patch, ReleaseSuffix releaseSuffix, int build) {
- return new CarAppVersion(major, minor, patch, releaseSuffix, build);
- }
-
- /**
- * Creates a {@link CarAppVersion} instance based on its string representation.
- *
- * <p>The string should be of the format major.minor.patch(-releaseSuffix.build). If the
- * string is malformed or {@code null}, {@code null} will be returned.
- */
- @Nullable
- public static CarAppVersion of(@NonNull String versionString) throws MalformedVersionException {
- return parseVersionString(versionString);
- }
-
- private static CarAppVersion parseVersionString(String versionString)
- throws MalformedVersionException {
- String[] versionSplit = versionString.split("-", -1);
- if (versionSplit.length > 2) {
- throw new MalformedVersionException(
- "Malformed version string (more than 1 \"-\" detected): " + versionString);
- }
-
- String mainVersion = versionSplit[0];
- String[] mainVersionSplit = mainVersion.split("\\.", -1);
-
- // Main version should be formatted as major.minor.patch
- if (mainVersionSplit.length != 3) {
- throw new MalformedVersionException(
- "Malformed version string (invalid main version format): " + versionString);
- }
-
- int major;
- int minor;
- int patch;
- ReleaseSuffix releaseSuffix = null;
- int build = 0;
-
- try {
- major = Integer.parseInt(mainVersionSplit[0]);
- minor = Integer.parseInt(mainVersionSplit[1]);
- patch = Integer.parseInt(mainVersionSplit[2]);
-
- String suffixVersion = versionSplit.length > 1 ? versionSplit[1] : null;
- if (suffixVersion != null) {
- String[] suffixVersionSplit = suffixVersion.split("\\.", -1);
-
- // Release suffix should be formatted as releaseSuffix.build
- if (suffixVersionSplit.length != 2) {
- throw new MalformedVersionException(
- "Malformed version string (invalid suffix version format): "
- + versionString);
- }
-
- try {
- releaseSuffix = ReleaseSuffix.fromString(suffixVersionSplit[0]);
- } catch (IllegalArgumentException e) {
- throw new MalformedVersionException(
- "Malformed version string (unsupported suffix): " + versionString, e);
- }
- build = Integer.parseInt(suffixVersionSplit[1]);
- }
-
- return new CarAppVersion(major, minor, patch, releaseSuffix, build);
- } catch (NumberFormatException exception) {
- throw new MalformedVersionException(
- "Malformed version string (unsupported characters): " + versionString,
- exception);
- }
- }
-
- private CarAppVersion(
- int major, int minor, int patch, @Nullable ReleaseSuffix releaseSuffix, int build) {
- this.mMajor = major;
- this.mMinor = minor;
- this.mPatch = patch;
- this.mReleaseSuffix = releaseSuffix;
- this.mBuild = build;
- }
-
- /** Returns the human-readable format of this version. */
- @NonNull
- @Override
- public String toString() {
- String versionString = String.format(Locale.US, MAIN_VERSION_FORMAT, mMajor, mMinor,
- mPatch);
- if (mReleaseSuffix != null) {
- versionString += String.format(Locale.US, SUFFIX_VERSION_FORMAT, mReleaseSuffix,
- mBuild);
- }
-
- return versionString;
- }
-
- /**
- * Checks whether this {@link CarAppVersion} is greater than or equal to {@code other}, which is
- * used to determine compatibility. Returns true if so, false otherwise.
- *
- * <p>The rules of comparison are as follow:
- *
- * <ul>
- * <li>If either versions are suffixed with {@link ReleaseSuffix#RELEASE_SUFFIX_EAP}, the
- * version strings have to be exact match to be considered compatible.
- * <li>The major version has to be greater or equal to be considered compatible.
- * <li>If major versions are equal, the minor version has to be greater or equal to be
- * considered compatible.
- * <li>If major.minor versions are equal, the patch version has to be greater or equal to
- * be considered compatible.
- * <li>If the major.minor.patch versions are equal, for stable versions release suffix
- * equals {@code null}, the instance is considered compatible with all
- * {@link ReleaseSuffix#RELEASE_SUFFIX_BETA} versions; for
- * {@link ReleaseSuffix#RELEASE_SUFFIX_BETA}, the instance is considered compatible iff the
- * other is a {@link ReleaseSuffix#RELEASE_SUFFIX_BETA} version and that the build is
- * greater or equal to the other version.
- * </ul>
- */
- public boolean isGreaterOrEqualTo(@NonNull CarAppVersion other) {
- // For EAP versions, we require an exact match.
- if (mReleaseSuffix == RELEASE_SUFFIX_EAP || other.mReleaseSuffix == RELEASE_SUFFIX_EAP) {
- return mMajor == other.mMajor
- && mMinor == other.mMinor
- && mPatch == other.mPatch
- && mReleaseSuffix == other.mReleaseSuffix
- && mBuild == other.mBuild;
- }
-
- int result = Integer.compare(mMajor, other.mMajor);
- // Major version differs, return.
- if (result != 0) {
- return result == 1;
- }
-
- // Minor version differs, return.
- result = Integer.compare(mMinor, other.mMinor);
- if (result != 0) {
- return result == 1;
- }
-
- // Patch version differs, return.
- result = Integer.compare(mPatch, other.mPatch);
- if (result != 0) {
- return result == 1;
- }
-
- if (mReleaseSuffix == null) {
- // A stable version (is considered compatible to any beta versions, the suffix
- // version is
- // ignored in those cases.
- return true;
- } else if (mReleaseSuffix == RELEASE_SUFFIX_BETA) {
- // Compatible if beta build is greater than other's beta build.
- if (other.mReleaseSuffix == RELEASE_SUFFIX_BETA) {
- return mBuild >= other.mBuild;
- } else {
- // Beta version is incompatible with stable version.
- return false;
- }
- } else {
- throw new IllegalStateException("Invalid release suffix: " + mReleaseSuffix);
- }
- }
-}
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 11837d2..41e3241 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
@@ -40,9 +40,12 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.StringDef;
+import androidx.car.app.annotations.RequiresCarApi;
import androidx.car.app.navigation.NavigationManager;
import androidx.car.app.utils.RemoteUtils;
import androidx.car.app.utils.ThreadUtils;
+import androidx.car.app.versioning.CarAppApiLevel;
+import androidx.car.app.versioning.CarAppApiLevels;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
@@ -81,22 +84,22 @@
*
* @hide
*/
- @StringDef({APP_SERVICE, CAR_SERVICE, NAVIGATION_SERVICE, SCREEN_MANAGER_SERVICE})
+ @StringDef({APP_SERVICE, CAR_SERVICE, NAVIGATION_SERVICE, SCREEN_SERVICE})
@Retention(RetentionPolicy.SOURCE)
public @interface CarServiceType {
}
/** Manages all app events such as invalidating the UI, showing a toast, etc. */
- public static final String APP_SERVICE = "app_manager";
+ public static final String APP_SERVICE = "app";
/**
* Manages all navigation events such as starting navigation when focus is granted, abandoning
* navigation when focus is lost, etc.
*/
- public static final String NAVIGATION_SERVICE = "navigation_manager";
+ public static final String NAVIGATION_SERVICE = "navigation";
/** Manages the screens of the app, including the screen stack. */
- public static final String SCREEN_MANAGER_SERVICE = "screen_manager";
+ public static final String SCREEN_SERVICE = "screen";
/**
* Internal usage only. Top level binder to host.
@@ -107,7 +110,8 @@
* Key for including a IStartCarApp in the notification {@link Intent}, for starting the app
* if it has not been opened yet.
*/
- public static final String START_CAR_APP_BINDER_KEY = "StartCarAppBinderKey";
+ public static final String EXTRA_START_CAR_APP_BINDER_KEY = "androidx.car.app.extra"
+ + ".START_CAR_APP_BINDER_KEY";
/**
* Standard action for navigating to a location.
@@ -121,9 +125,12 @@
private final NavigationManager mNavigationManager;
private final ScreenManager mScreenManager;
private final OnBackPressedDispatcher mOnBackPressedDispatcher;
-
private final HostDispatcher mHostDispatcher;
+ /** API level, updated once host connection handshake is completed. */
+ @CarAppApiLevel
+ private int mCarAppApiLevel = CarAppApiLevels.UNKNOWN;
+
/** @hide */
@NonNull
@RestrictTo(LIBRARY)
@@ -143,13 +150,13 @@
* <dd>An {@link AppManager} for communication between the app and the host.
* <dt>{@link #NAVIGATION_SERVICE}
* <dd>A {@link NavigationManager} for management of navigation updates.
- * <dt>{@link #SCREEN_MANAGER_SERVICE}
+ * <dt>{@link #SCREEN_SERVICE}
* <dd>A {@link ScreenManager} for management of {@link Screen}s.
* </dl>
*
* @param name The name of the car service requested. This should be one of
* {@link #APP_SERVICE},
- * {@link #NAVIGATION_SERVICE} or {@link #SCREEN_MANAGER_SERVICE}.
+ * {@link #NAVIGATION_SERVICE} or {@link #SCREEN_SERVICE}.
* @return The car service instance.
* @throws IllegalArgumentException if {@code name} does not refer to a valid car service.
* @throws NullPointerException if {@code name} is {@code null}.
@@ -162,7 +169,7 @@
return mAppManager;
case NAVIGATION_SERVICE:
return mNavigationManager;
- case SCREEN_MANAGER_SERVICE:
+ case SCREEN_SERVICE:
return mScreenManager;
default: // fall out
}
@@ -206,7 +213,7 @@
} else if (serviceClass.isInstance(mNavigationManager)) {
return NAVIGATION_SERVICE;
} else if (serviceClass.isInstance(mScreenManager)) {
- return SCREEN_MANAGER_SERVICE;
+ return SCREEN_SERVICE;
}
throw new IllegalArgumentException("The class does not correspond to a car service.");
@@ -215,8 +222,7 @@
/**
* Starts a car app on the car screen.
*
- * <p>The target application will get the {@link Intent} via
- * {@link CarAppService#onCreateScreen}
+ * <p>The target application will get the {@link Intent} via {@link Session#onCreateScreen}
* or {@link CarAppService#onNewIntent}.
*
* <p>Supported {@link Intent}s:
@@ -282,7 +288,7 @@
IBinder binder = null;
Bundle extras = notificationIntent.getExtras();
if (extras != null) {
- binder = extras.getBinder(START_CAR_APP_BINDER_KEY);
+ binder = extras.getBinder(EXTRA_START_CAR_APP_BINDER_KEY);
}
if (binder == null) {
throw new IllegalArgumentException("Notification intent missing expected extra");
@@ -301,10 +307,10 @@
/**
* Requests to finish the car app.
*
- * <p>Call this when your app is done and should be closed.
+ * <p>Call this when your app is done and should be closed. The {@link Session} corresponding
+ * to this {@link CarContext} will become {@code State.DESTROYED}.
*
- * <p>At some point after this call, {@link CarAppService#onCarAppFinished} will be called, and
- * eventually the OS will destroy your {@link CarAppService}.
+ * <p>At some point after this call, the OS will destroy your {@link CarAppService}.
*/
public void finishCarApp() {
mHostDispatcher.dispatch(
@@ -374,6 +380,22 @@
}
/**
+ * Updates context information based on the information provided during connection handshake
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ @MainThread
+ void onHandshakeComplete(HandshakeInfo handshakeInfo) {
+ int carAppApiLevel = handshakeInfo.getHostCarAppApiLevel();
+ if (!CarAppApiLevels.isValid(carAppApiLevel)) {
+ throw new IllegalArgumentException("Invalid Car App API level received: "
+ + carAppApiLevel);
+ }
+ mCarAppApiLevel = carAppApiLevel;
+ }
+
+ /**
* Attaches the base {@link Context} for this {@link CarContext} by creating a new display
* context using {@link #createDisplayContext} with a {@link VirtualDisplay} created using
* the metrics from the provided {@link Configuration}, and then also calling {@link
@@ -431,6 +453,39 @@
mHostDispatcher.resetHosts();
}
+ /**
+ * Retrieves the API level negotiated with the host.
+ * <p>
+ * API levels are used during client and host connection handshake to negotiate a common set of
+ * elements that both processes can understand. Different devices might have different host
+ * versions. Each of these hosts will support a
+ * range of API levels, as a way to provide backwards compatibility.
+ * <p>
+ * Applications can also provide forward compatibility, by declaring support for a
+ * {@link AppInfo#getMinCarAppApiLevel()} lower than {@link AppInfo#getLatestCarAppApiLevel()}.
+ * See {@link AppInfo#getMinCarAppApiLevel()} for more details.
+ * <p>
+ * Clients must ensure no elements annotated with a {@link RequiresCarApi} value higher
+ * than {@link #getCarAppApiLevel()} is used at runtime.
+ * <p>
+ * Please refer to {@link RequiresCarApi} description for more details on how to
+ * implement forward compatibility.
+ *
+ * @return a value between {@link AppInfo#getMinCarAppApiLevel()} and
+ * {@link AppInfo#getLatestCarAppApiLevel()}. In case of incompatibility, the host will
+ * disconnect from the service before completing the handshake.
+ *
+ * @throws IllegalStateException if invoked before the connection handshake with the host has
+ * been completed (for example, before {@link Session#onCreateScreen(Intent)}).
+ */
+ @CarAppApiLevel
+ public int getCarAppApiLevel() {
+ if (mCarAppApiLevel == CarAppApiLevels.UNKNOWN) {
+ throw new IllegalStateException("Car App API level hasn't been established yet");
+ }
+ return mCarAppApiLevel;
+ }
+
/** @hide */
@RestrictTo(LIBRARY_GROUP) // Restrict to testing library
@SuppressWarnings({
@@ -442,7 +497,7 @@
this.mHostDispatcher = hostDispatcher;
mAppManager = AppManager.create(this, hostDispatcher);
- mNavigationManager = NavigationManager.create(hostDispatcher);
+ mNavigationManager = NavigationManager.create(this, hostDispatcher);
mScreenManager = ScreenManager.create(this, lifecycle);
mOnBackPressedDispatcher =
new OnBackPressedDispatcher(() -> getCarService(ScreenManager.class).pop());
diff --git a/car/app/app/src/main/java/androidx/car/app/HandshakeInfo.java b/car/app/app/src/main/java/androidx/car/app/HandshakeInfo.java
index 8d3f137..8b92f28 100644
--- a/car/app/app/src/main/java/androidx/car/app/HandshakeInfo.java
+++ b/car/app/app/src/main/java/androidx/car/app/HandshakeInfo.java
@@ -33,18 +33,25 @@
public class HandshakeInfo {
@Nullable
private final String mHostPackageName;
+ private final int mHostCarAppApiLevel;
- public HandshakeInfo(@NonNull String hostPackageName) {
- this.mHostPackageName = hostPackageName;
+ public HandshakeInfo(@NonNull String hostPackageName, int hostCarAppApiLevel) {
+ mHostPackageName = hostPackageName;
+ mHostCarAppApiLevel = hostCarAppApiLevel;
}
// Used for serialization
public HandshakeInfo() {
mHostPackageName = null;
+ mHostCarAppApiLevel = 0;
}
@NonNull
public String getHostPackageName() {
return requireNonNull(mHostPackageName);
}
+
+ public int getHostCarAppApiLevel() {
+ return mHostCarAppApiLevel;
+ }
}
diff --git a/car/app/app/src/main/java/androidx/car/app/HostInfo.java b/car/app/app/src/main/java/androidx/car/app/HostInfo.java
index 5b1dad2..7174e18 100644
--- a/car/app/app/src/main/java/androidx/car/app/HostInfo.java
+++ b/car/app/app/src/main/java/androidx/car/app/HostInfo.java
@@ -19,9 +19,10 @@
import static java.util.Objects.requireNonNull;
import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
/**
- * Container class for information about the host the service is connected to.
+ * Container class for information about the host the app is connected to.
*
* <p>Apps can use this information to determine how they will respond to the host. For example, a
* host which is not recognized could receive a message screen while an authorized host could
@@ -29,16 +30,24 @@
*
* <p>The package name and uid can used to query the system package manager for a signature or to
* determine if the host has a system signature.
+ *
+ * <p>The host API level can be used to adjust the models exchanged with the host to those valid
+ * for the specific host version the app is connected to.
*/
public class HostInfo {
@NonNull
private final String mPackageName;
private final int mUid;
- /** Constructs an instance of the HostInfo from the required package name and uid. */
+ /**
+ * Constructs an instance of the HostInfo from the required package name, uid and API level.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public HostInfo(@NonNull String packageName, int uid) {
- this.mPackageName = requireNonNull(packageName);
- this.mUid = uid;
+ mPackageName = requireNonNull(packageName);
+ mUid = uid;
}
/** Retrieves the package name of the host. */
diff --git a/car/app/app/src/main/java/androidx/car/app/ScreenManager.java b/car/app/app/src/main/java/androidx/car/app/ScreenManager.java
index 56b3365..7dafbc2 100644
--- a/car/app/app/src/main/java/androidx/car/app/ScreenManager.java
+++ b/car/app/app/src/main/java/androidx/car/app/ScreenManager.java
@@ -58,7 +58,7 @@
* @throws NullPointerException if the method is called before a {@link Screen} has been
* pushed to the stack via {@link #push}, or
* {@link #pushForResult}, or returning a {@link Screen} from
- * {@link CarAppService#onCreateScreen}.
+ * {@link Session#onCreateScreen}.
*/
@NonNull
public Screen getTop() {
diff --git a/car/app/app/src/main/java/androidx/car/app/SearchListener.java b/car/app/app/src/main/java/androidx/car/app/SearchListener.java
deleted file mode 100644
index ed814d4..0000000
--- a/car/app/app/src/main/java/androidx/car/app/SearchListener.java
+++ /dev/null
@@ -1,41 +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.car.app;
-
-import androidx.annotation.NonNull;
-
-/** A listener for search updates. */
-public interface SearchListener {
- /**
- * Notifies the current {@code searchText}.
- *
- * <p>The host may invoke this callback as the user types a search text. The frequency of these
- * updates is not guaranteed to be after every individual keystroke. The host may decide to wait
- * for several keystrokes before sending a single update.
- *
- * @param searchText the current search text that the user has typed.
- */
- void onSearchTextChanged(@NonNull String searchText);
-
- /**
- * Notifies that the user has submitted the search and the given {@code searchText} is the final
- * term.
- *
- * @param searchText the search text that the user typed.
- */
- void onSearchSubmitted(@NonNull String searchText);
-}
diff --git a/car/app/app/src/main/java/androidx/car/app/Session.java b/car/app/app/src/main/java/androidx/car/app/Session.java
new file mode 100644
index 0000000..e77907a
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/Session.java
@@ -0,0 +1,112 @@
+/*
+ * 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.car.app;
+
+import android.content.Intent;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+/**
+ * The base class for implementing a session for a car app.
+ */
+public abstract class Session implements LifecycleOwner {
+ private final LifecycleRegistry mRegistry = new LifecycleRegistry(this);
+ private final CarContext mCarContext = CarContext.create(mRegistry);
+
+ /**
+ * Requests the first {@link Screen} for the application.
+ *
+ * <p>Once the method returns, {@link Screen#onGetTemplate()} will be called on the
+ * {@link Screen} returned, and the app will be displayed on the car screen.
+ *
+ * <p>To pre-seed a back stack, you can push {@link Screen}s onto the stack, via {@link
+ * ScreenManager#push} during this method call.
+ *
+ * <p>Called by the system, do not call this method directly.
+ *
+ * @param intent the intent that was used to start this app. If the app was started with a
+ * call to {@link CarContext#startCarApp}, this intent will be equal to the
+ * intent passed to that method.
+ */
+ @NonNull
+ public abstract Screen onCreateScreen(@NonNull Intent intent);
+
+ /**
+ * Returns the {@link Session}'s {@link Lifecycle}.
+ *
+ * <p>Here are some of the ways you can use the sessions's {@link Lifecycle}:
+ *
+ * <ul>
+ * <li>Observe its {@link Lifecycle} by calling {@link Lifecycle#addObserver}. You can use the
+ * {@link androidx.lifecycle.LifecycleObserver} to take specific actions whenever the
+ * {@link Screen} receives different {@link Lifecycle.Event}s.
+ * <li>Use this {@link CarAppService} to observe {@link androidx.lifecycle.LiveData}s that
+ * may drive the backing data for your application.
+ * </ul>
+ *
+ * <p>What each lifecycle related event means for a session:
+ *
+ * <dl>
+ * <dt>{@link Lifecycle.Event#ON_CREATE}
+ * <dd>The session has just been launched, and this session is being initialized. {@link
+ * #onCreateScreen} will be called at a point after this call.
+ * <dt>{@link #onCreateScreen}
+ * <dd>The host is ready for this session to create the first {@link Screen} so that it can
+ * display its template.
+ * <dt>{@link Lifecycle.Event#ON_START}
+ * <dd>The application is now visible in the car screen.
+ * <dt>{@link Lifecycle.Event#ON_RESUME}
+ * <dd>The user can now interact with this application.
+ * <dt>{@link Lifecycle.Event#ON_PAUSE}
+ * <dd>The user can no longer interact with this application.
+ * <dt>{@link Lifecycle.Event#ON_STOP}
+ * <dd>The application is no longer visible.
+ * <dt>{@link Lifecycle.Event#ON_DESTROY}
+ * <dd>The OS has now destroyed this {@link Session} instance, and it is no longer
+ * valid.
+ * </dl>
+ *
+ * <p>Listeners that are added in {@link Lifecycle.Event#ON_START}, should be removed in {@link
+ * Lifecycle.Event#ON_STOP}.
+ *
+ * <p>Listeners that are added in {@link Lifecycle.Event#ON_CREATE} should be removed in {@link
+ * Lifecycle.Event#ON_DESTROY}.
+ *
+ * @see androidx.lifecycle.LifecycleObserver
+ */
+ @NonNull
+ @Override
+ public Lifecycle getLifecycle() {
+ return mRegistry;
+ }
+
+ /**
+ * Returns the {@link CarContext} for this session.
+ *
+ * <p><b>The {@link CarContext} is not fully initialized until this session's {@link
+ * Lifecycle.State} is at least {@link Lifecycle.State#CREATED}</b>
+ *
+ * @see #getLifecycle
+ */
+ @NonNull
+ public final CarContext getCarContext() {
+ return mCarContext;
+ }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/annotations/RequiresCarApi.java b/car/app/app/src/main/java/androidx/car/app/annotations/RequiresCarApi.java
new file mode 100644
index 0000000..4201a81
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/annotations/RequiresCarApi.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app.annotations;
+
+import androidx.car.app.CarContext;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Defines the minimum API level required to be able to use this model, field or method.
+ * <p>
+ * Before using any of this elements, application must check that
+ * {@link CarContext#getCarAppApiLevel()} is equal or greater than the value of this annotation.
+ * <p>
+ * For example, if an application wants to use a newer template "Foo" marked with
+ * <code>@RequiresHostApiLevel(2)</code> while maintain backwards compatibility with older hosts
+ * by using an older template "Bar" (<code>@RequiresHostApiLevel(1)</code>), they can do:
+ *
+ * <pre>
+ * if (getCarContext().getCarApiLevel() >= 2) {
+ * // Use new feature
+ * return Foo.Builder()....;
+ * } else {
+ * // Use supported fallback
+ * return Bar.Builder()....;
+ * }
+ * </pre>
+ *
+ * If a certain model or method has no {@link RequiresCarApi} annotation, it is assumed to
+ * be available in all car API levels.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD})
+public @interface RequiresCarApi {
+
+ /**
+ * The minimum API level required to be able to use this model, field or method. Applications
+ * shouldn't use any elements annotated with a {@link RequiresCarApi} greater than
+ * {@link CarContext#getCarAppApiLevel()}
+ */
+ int value();
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/GridItem.java b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
index 32e9482..7737be0 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
@@ -98,9 +98,9 @@
}
/** Returns the title of the grid item. */
- @Nullable
+ @NonNull
public CarText getTitle() {
- return mTitle;
+ return requireNonNull(mTitle);
}
/** Returns the list of text below the title. */
@@ -208,10 +208,19 @@
@Nullable
private OnClickListenerWrapper mOnClickListener;
- /** Sets the title of the grid item, or {@code null} to not show the title. */
+ /**
+ * Sets the title of the row.
+ *
+ * @throws NullPointerException if {@code title} is {@code null}.
+ * @throws IllegalArgumentException if {@code title} is empty.
+ */
@NonNull
- public Builder setTitle(@Nullable CharSequence title) {
- this.mTitle = title == null ? null : CarText.create(title);
+ public Builder setTitle(@NonNull CharSequence title) {
+ CarText titleText = CarText.create(requireNonNull(title));
+ if (titleText.isEmpty()) {
+ throw new IllegalArgumentException("The title cannot be null or empty");
+ }
+ this.mTitle = titleText;
return this;
}
@@ -221,9 +230,7 @@
*
* <h2>Text Wrapping</h2>
*
- * The string added with {@link #setText} is truncated at the end to fit in a single line
- * below
- * the title.
+ * This text is truncated at the end to fit in a single line below the title.
*/
@NonNull
public Builder setText(@Nullable CharSequence text) {
@@ -314,9 +321,8 @@
throw new IllegalStateException("An image must be set on the grid item");
}
- if (mTitle == null && mText != null) {
- throw new IllegalStateException(
- "If a grid item doesn't have a title, it must not have a text set");
+ if (mTitle == null) {
+ throw new IllegalStateException("A title must be set on the grid item");
}
if (mToggle != null && mOnClickListener != null) {
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ItemList.java b/car/app/app/src/main/java/androidx/car/app/model/ItemList.java
index 367931b..10a898d 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/ItemList.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/ItemList.java
@@ -26,11 +26,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.IOnDoneCallback;
-import androidx.car.app.IOnItemVisibilityChangedListener;
-import androidx.car.app.IOnSelectedListener;
import androidx.car.app.OnDoneCallback;
-import androidx.car.app.OnItemVisibilityChangedListenerWrapper;
-import androidx.car.app.OnSelectedListenerWrapper;
import androidx.car.app.WrappedRuntimeException;
import androidx.car.app.utils.RemoteUtils;
@@ -237,7 +233,7 @@
* @see #setSelectedIndex(int)
*/
@NonNull
- @SuppressLint({"ExecutorRegistration"})
+ @SuppressLint("ExecutorRegistration")
public Builder setOnSelectedListener(@Nullable OnSelectedListener onSelectedListener) {
this.mOnSelectedListener =
onSelectedListener == null ? null : createOnSelectedListener(
diff --git a/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java
index 630ead2..baa90dc 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/MessageTemplate.java
@@ -93,7 +93,7 @@
}
@Nullable
- public ActionList getActionList() {
+ public ActionList getActions() {
return mActionList;
}
@@ -273,8 +273,6 @@
* @throws NullPointerException if {@code actions} is {@code null}.
*/
@NonNull
- // TODO(shiufai): consider rename to match getter's name (e.g. setActionList or getActions).
- @SuppressLint("MissingGetterMatchingBuilder")
public Builder setActions(@NonNull List<Action> actions) {
mActionList = ActionList.create(requireNonNull(actions));
return this;
diff --git a/car/app/app/src/main/java/androidx/car/app/OnCheckedChangeListenerWrapper.java b/car/app/app/src/main/java/androidx/car/app/model/OnCheckedChangeListenerWrapper.java
similarity index 93%
rename from car/app/app/src/main/java/androidx/car/app/OnCheckedChangeListenerWrapper.java
rename to car/app/app/src/main/java/androidx/car/app/model/OnCheckedChangeListenerWrapper.java
index 4aaca10..603323c 100644
--- a/car/app/app/src/main/java/androidx/car/app/OnCheckedChangeListenerWrapper.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnCheckedChangeListenerWrapper.java
@@ -14,9 +14,10 @@
* limitations under the License.
*/
-package androidx.car.app;
+package androidx.car.app.model;
import androidx.annotation.NonNull;
+import androidx.car.app.OnDoneCallback;
/**
* A host-side interface for reporting to clients that the checked state has changed.
diff --git a/car/app/app/src/main/java/androidx/car/app/OnItemVisibilityChangedListenerWrapper.java b/car/app/app/src/main/java/androidx/car/app/model/OnItemVisibilityChangedListenerWrapper.java
similarity index 95%
rename from car/app/app/src/main/java/androidx/car/app/OnItemVisibilityChangedListenerWrapper.java
rename to car/app/app/src/main/java/androidx/car/app/model/OnItemVisibilityChangedListenerWrapper.java
index 5d7175f6..62e3e5b 100644
--- a/car/app/app/src/main/java/androidx/car/app/OnItemVisibilityChangedListenerWrapper.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnItemVisibilityChangedListenerWrapper.java
@@ -14,9 +14,10 @@
* limitations under the License.
*/
-package androidx.car.app;
+package androidx.car.app.model;
import androidx.annotation.NonNull;
+import androidx.car.app.OnDoneCallback;
/**
* A host-side interface for reporting to clients that the visibility state has changed.
diff --git a/car/app/app/src/main/java/androidx/car/app/OnSelectedListenerWrapper.java b/car/app/app/src/main/java/androidx/car/app/model/OnSelectedListenerWrapper.java
similarity index 94%
rename from car/app/app/src/main/java/androidx/car/app/OnSelectedListenerWrapper.java
rename to car/app/app/src/main/java/androidx/car/app/model/OnSelectedListenerWrapper.java
index 812d211..92e3006 100644
--- a/car/app/app/src/main/java/androidx/car/app/OnSelectedListenerWrapper.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnSelectedListenerWrapper.java
@@ -14,9 +14,10 @@
* limitations under the License.
*/
-package androidx.car.app;
+package androidx.car.app.model;
import androidx.annotation.NonNull;
+import androidx.car.app.OnDoneCallback;
/**
* A host-side interface for reporting to clients that an item was selected.
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Pane.java b/car/app/app/src/main/java/androidx/car/app/model/Pane.java
index 513cac3..08e486b 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Pane.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Pane.java
@@ -18,8 +18,6 @@
import static java.util.Objects.requireNonNull;
-import android.annotation.SuppressLint;
-
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -52,7 +50,7 @@
* Returns the list of {@link Action}s displayed alongside the {@link Row}s in this pane.
*/
@Nullable
- public ActionList getActionList() {
+ public ActionList getActions() {
return mActionList;
}
@@ -157,8 +155,6 @@
* @throws NullPointerException if {@code actions} is {@code null}.
*/
@NonNull
- // TODO(shiufai): consider rename to match getter's name (e.g. setActionList or getActions).
- @SuppressLint("MissingGetterMatchingBuilder")
public Builder setActions(@NonNull List<Action> actions) {
mActionList = ActionList.create(requireNonNull(actions));
return this;
diff --git a/car/app/app/src/main/java/androidx/car/app/model/PlaceListMapTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/PlaceListMapTemplate.java
index ada9d74..187c3c8 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/PlaceListMapTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/PlaceListMapTemplate.java
@@ -220,8 +220,7 @@
/**
* Sets the {@link Action} that will be displayed in the header of the template, or
- * {@code null}
- * to not display an action.
+ * {@code null} to not display an action.
*
* <h4>Requirements</h4>
*
@@ -242,8 +241,7 @@
/**
* Sets the {@link CharSequence} to show as the template's title, or {@code null} to not
- * display
- * a title.
+ * display a title.
*/
@NonNull
public Builder setTitle(@Nullable CharSequence title) {
@@ -308,8 +306,7 @@
*
* This template allows up to 2 {@link Action}s in its {@link ActionStrip}. Of the 2 allowed
* {@link Action}s, one of them can contain a title as set via
- * {@link Action.Builder#setTitle}.
- * Otherwise, only {@link Action}s with icons are allowed.
+ * {@link Action.Builder#setTitle}. Otherwise, only {@link Action}s with icons are allowed.
*
* @throws IllegalArgumentException if {@code actionStrip} does not meet the template's
* requirements.
@@ -328,13 +325,11 @@
* <p>The anchor marker is displayed differently from other markers by the host.
*
* <p>If not {@code null}, an anchor marker will be shown at the specified {@link LatLng}
- * on the
- * map. The camera will adapt to always have the anchor marker visible within its viewport,
- * along with other places' markers from {@link Row} that are currently visible in the
- * {@link
- * Pane}. This can be used to provide a reference point on the map (e.g. the center of a
- * search
- * region) as the user pages through the {@link Pane}'s markers, for example.
+ * on the map. The camera will adapt to always have the anchor marker visible within its
+ * viewport, along with other places' markers from {@link Row} that are currently visible
+ * in the {@link Pane}. This can be used to provide a reference point on the map (e.g.
+ * the center of a search region) as the user pages through the {@link Pane}'s markers,
+ * for example.
*/
@NonNull
public Builder setAnchor(@Nullable Place anchor) {
@@ -350,8 +345,7 @@
* Either a header {@link Action} or title must be set on the template.
*
* @throws IllegalArgumentException if the template is in a loading state but the list is
- * set,
- * or vice versa.
+ * set, or vice versa.
* @throws IllegalStateException if the template does not have either a title or header
* {@link Action} set.
*/
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Row.java b/car/app/app/src/main/java/androidx/car/app/model/Row.java
index 1099af0..34a0e23 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Row.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Row.java
@@ -302,7 +302,7 @@
*
* <h4>Text Wrapping</h4>
*
- * Each string added with {@link #addText} will not wrap more than 1 line in the UI, with
+ * Each string added with this method will not wrap more than 1 line in the UI, with
* one exception: if the template allows a maximum number of text strings larger than 1, and
* the app adds a single text string, then this string will wrap up to the maximum.
*
diff --git a/car/app/app/src/main/java/androidx/car/app/SearchListenerWrapper.java b/car/app/app/src/main/java/androidx/car/app/model/SearchListenerWrapper.java
similarity index 95%
rename from car/app/app/src/main/java/androidx/car/app/SearchListenerWrapper.java
rename to car/app/app/src/main/java/androidx/car/app/model/SearchListenerWrapper.java
index 050ac89..d331059 100644
--- a/car/app/app/src/main/java/androidx/car/app/SearchListenerWrapper.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/SearchListenerWrapper.java
@@ -14,9 +14,10 @@
* limitations under the License.
*/
-package androidx.car.app;
+package androidx.car.app.model;
import androidx.annotation.NonNull;
+import androidx.car.app.OnDoneCallback;
/**
* A host-side interface for reporting to search updates to clients.
diff --git a/car/app/app/src/main/java/androidx/car/app/model/SearchTemplate.java b/car/app/app/src/main/java/androidx/car/app/model/SearchTemplate.java
index 18976ce..e9c7724 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/SearchTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/SearchTemplate.java
@@ -28,11 +28,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.IOnDoneCallback;
-import androidx.car.app.ISearchListener;
import androidx.car.app.OnDoneCallback;
import androidx.car.app.Screen;
-import androidx.car.app.SearchListener;
-import androidx.car.app.SearchListenerWrapper;
import androidx.car.app.WrappedRuntimeException;
import androidx.car.app.utils.RemoteUtils;
@@ -49,6 +46,29 @@
* results as the user types without the templates being counted against the quota.
*/
public final class SearchTemplate implements Template {
+
+ /** A listener for search updates. */
+ public interface SearchListener {
+ /**
+ * Notifies the current {@code searchText}.
+ *
+ * <p>The host may invoke this callback as the user types a search text. The frequency of
+ * these updates is not guaranteed to be after every individual keystroke. The host may
+ * decide to wait for several keystrokes before sending a single update.
+ *
+ * @param searchText the current search text that the user has typed.
+ */
+ void onSearchTextChanged(@NonNull String searchText);
+
+ /**
+ * Notifies that the user has submitted the search and the given {@code searchText} is
+ * the final term.
+ *
+ * @param searchText the search text that the user typed.
+ */
+ void onSearchSubmitted(@NonNull String searchText);
+ }
+
@Keep
private final boolean mIsLoading;
@Keep
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Toggle.java b/car/app/app/src/main/java/androidx/car/app/model/Toggle.java
index f704b7a..c657aec 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Toggle.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Toggle.java
@@ -25,9 +25,7 @@
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.car.app.IOnCheckedChangeListener;
import androidx.car.app.IOnDoneCallback;
-import androidx.car.app.OnCheckedChangeListenerWrapper;
import androidx.car.app.OnDoneCallback;
import androidx.car.app.WrappedRuntimeException;
import androidx.car.app.utils.RemoteUtils;
diff --git a/car/app/app/src/main/java/androidx/car/app/model/constraints/RowListConstraints.java b/car/app/app/src/main/java/androidx/car/app/model/constraints/RowListConstraints.java
index 5d20a94..393e555 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/constraints/RowListConstraints.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/constraints/RowListConstraints.java
@@ -189,7 +189,7 @@
* @throws IllegalArgumentException if the constraints are not met.
*/
public void validateOrThrow(@NonNull Pane pane) {
- ActionList actions = pane.getActionList();
+ ActionList actions = pane.getActions();
if (actions != null && actions.getList().size() > mMaxActions) {
throw new IllegalArgumentException(
"The number of actions on the pane exceeded the supported max of "
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java
index 787fa0e..2b78cae 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManager.java
@@ -23,6 +23,7 @@
import static java.util.Objects.requireNonNull;
import android.annotation.SuppressLint;
+import android.os.Looper;
import android.util.Log;
import androidx.annotation.MainThread;
@@ -38,6 +39,9 @@
import androidx.car.app.serialization.Bundleable;
import androidx.car.app.serialization.BundlerException;
import androidx.car.app.utils.RemoteUtils;
+import androidx.core.content.ContextCompat;
+
+import java.util.concurrent.Executor;
/**
* Manager for communicating navigation related events with the host.
@@ -51,17 +55,20 @@
* called.
*
* <p>Navigation apps must also register a {@link NavigationManagerListener} to handle callbacks to
- * {@link NavigationManagerListener#stopNavigation()} issued by the host.
+ * {@link NavigationManagerListener#onStopNavigation()} issued by the host.
*/
public class NavigationManager {
private static final String TAG = "NavigationManager";
- private final INavigationManager.Stub mNavigationmanager;
+ private final CarContext mCarContext;
+ private final INavigationManager.Stub mNavigationManager;
private final HostDispatcher mHostDispatcher;
// Guarded by main thread access.
@Nullable
- private NavigationManagerListener mListener;
+ private NavigationManagerListener mNavigationManagerListener;
+ @Nullable
+ private Executor mNavigationManagerListenerExecutor;
private boolean mIsNavigating;
private boolean mIsAutoDriveEnabled;
@@ -74,7 +81,7 @@
* <p>This method should only be invoked once the navigation app has called {@link
* #navigationStarted()}, or else the updates will be dropped by the host. Once the app has
* called {@link #navigationEnded()} or received
- * {@link NavigationManagerListener#stopNavigation()} it should stop sending updates.
+ * {@link NavigationManagerListener#onStopNavigation()} it should stop sending updates.
*
* <p>As the location changes, and in accordance with speed and rounded distance changes, the
* {@link TravelEstimate}s in the provided {@link Trip} should be rebuilt and this method called
@@ -86,13 +93,15 @@
* androidx.car.app.navigation.model.Maneuver}s of unknown type may be skipped while on other
* displays the associated icon may be shown.
*
+ * @param trip destination, steps, and trip estimates to be sent to the host
+ *
* @throws HostException if the call is invoked by an app that is not declared as
- * a navigation app in the manifest.
+ * a navigation app in the manifest
* @throws IllegalStateException if the call occurs when navigation is not started. See
- * {@link #navigationStarted()} for more info.
+ * {@link #navigationStarted()} for more info
* @throws IllegalArgumentException if any of the destinations, steps, or trip position is
- * not well formed.
- * @throws IllegalStateException if the current thread is not the main thread.
+ * not well formed
+ * @throws IllegalStateException if the current thread is not the main thread
*/
@MainThread
public void updateTrip(@NonNull Trip trip) {
@@ -118,25 +127,59 @@
}
/**
- * Sets a listener to start receiving navigation manager events, or {@code null} to clear the
- * listener.
+ * Sets a listener to start receiving navigation manager events.
*
- * @throws IllegalStateException if {@code null} is passed in while navigation is started. See
+ * Note that the listener will be executed on the main thread using
+ * {@link Looper#getMainLooper()}. To specify the execution thread, use
+ * {@link #setNavigationManagerListener(Executor, NavigationManagerListener)}.
+ *
+ * @param listener the {@link NavigationManagerListener} to use
+ *
+ * @throws IllegalStateException if the current thread is not the main thread
+ */
+ @SuppressLint("ExecutorRegistration")
+ @MainThread
+ public void setNavigationManagerListener(@NonNull NavigationManagerListener listener) {
+ checkMainThread();
+ Executor executor = ContextCompat.getMainExecutor(mCarContext);
+ setNavigationManagerListener(executor, listener);
+ }
+
+ /**
+ * Sets a listener to start receiving navigation manager events.
+ *
+ * @param executor the executor which will be used for invoking the listener
+ * @param listener the {@link NavigationManagerListener} to use
+ *
+ * @throws IllegalStateException if the current thread is not the main thread.
+ */
+ @MainThread
+ public void setNavigationManagerListener(@NonNull /* @CallbackExecutor */ Executor executor,
+ @NonNull NavigationManagerListener listener) {
+ checkMainThread();
+
+ mNavigationManagerListenerExecutor = executor;
+ mNavigationManagerListener = listener;
+ if (mIsAutoDriveEnabled) {
+ onAutoDriveEnabled();
+ }
+ }
+
+ /**
+ * Clears the listener for receiving navigation manager events.
+ *
+ * @throws IllegalStateException if navigation is started. See
* {@link #navigationStarted()} for more info.
* @throws IllegalStateException if the current thread is not the main thread.
*/
- // TODO(rampara): Add Executor parameter.
- @SuppressLint("ExecutorRegistration")
@MainThread
- public void setListener(@Nullable NavigationManagerListener listener) {
+ public void clearNavigationManagerListener() {
checkMainThread();
- if (mIsNavigating && listener == null) {
+ if (mIsNavigating) {
throw new IllegalStateException("Removing listener while navigating");
}
- this.mListener = listener;
- if (mIsAutoDriveEnabled && listener != null) {
- listener.onAutoDriveEnabled();
- }
+ mNavigationManagerListenerExecutor = null;
+ mNavigationManagerListener = null;
}
/**
@@ -146,10 +189,11 @@
* the host. The app must call this method to inform the system that it has started
* navigation in response to user action.
*
- * <p>This function can only called if {@link #setListener(NavigationManagerListener)} has been
+ * <p>This function can only called if
+ * {@link #setNavigationManagerListener(NavigationManagerListener)} has been
* called with a non-{@code null} value. The listener is required so that a signal to stop
* navigation from the host can be handled using
- * {@link NavigationManagerListener#stopNavigation()}.
+ * {@link NavigationManagerListener#onStopNavigation()}.
*
* <p>This method is idempotent.
*
@@ -162,7 +206,7 @@
if (mIsNavigating) {
return;
}
- if (mListener == null) {
+ if (mNavigationManagerListener == null) {
throw new IllegalStateException("No listener has been set");
}
mIsNavigating = true;
@@ -209,8 +253,9 @@
*/
@RestrictTo(LIBRARY)
@NonNull
- public static NavigationManager create(@NonNull HostDispatcher hostDispatcher) {
- return new NavigationManager(hostDispatcher);
+ public static NavigationManager create(@NonNull CarContext carContext,
+ @NonNull HostDispatcher hostDispatcher) {
+ return new NavigationManager(carContext, hostDispatcher);
}
/**
@@ -221,7 +266,7 @@
@RestrictTo(LIBRARY)
@NonNull
public INavigationManager.Stub getIInterface() {
- return mNavigationmanager;
+ return mNavigationManager;
}
/**
@@ -231,19 +276,20 @@
*/
@RestrictTo(LIBRARY)
@MainThread
- public void stopNavigation() {
+ public void onStopNavigation() {
checkMainThread();
if (!mIsNavigating) {
return;
}
mIsNavigating = false;
- requireNonNull(mListener).stopNavigation();
+ requireNonNull(mNavigationManagerListenerExecutor).execute(() -> {
+ requireNonNull(mNavigationManagerListener).onStopNavigation();
+ });
}
/**
- * Signifies that from this point, until {@link
- * androidx.car.app.CarAppService#onCarAppFinished} is called, any navigation
- * should automatically start driving to the destination as if the user was moving.
+ * Signifies that from this point, until {@link CarContext#finishCarApp()} is called, any
+ * navigation should automatically start driving to the destination as if the user was moving.
*
* <p>This is used in a testing environment, allowing testing the navigation app's navigation
* capabilities without being in a car.
@@ -255,9 +301,11 @@
public void onAutoDriveEnabled() {
checkMainThread();
mIsAutoDriveEnabled = true;
- if (mListener != null) {
+ if (mNavigationManagerListener != null) {
Log.d(TAG, "Executing onAutoDriveEnabled");
- mListener.onAutoDriveEnabled();
+ requireNonNull(mNavigationManagerListenerExecutor).execute(() -> {
+ mNavigationManagerListener.onAutoDriveEnabled();
+ });
} else {
Log.w(TAG, "NavigationManagerListener not set, skipping onAutoDriveEnabled");
}
@@ -266,14 +314,17 @@
/** @hide */
@RestrictTo(LIBRARY_GROUP) // Restrict to testing library
@SuppressWarnings({"methodref.receiver.bound.invalid"})
- protected NavigationManager(@NonNull HostDispatcher hostDispatcher) {
- this.mHostDispatcher = requireNonNull(hostDispatcher);
- mNavigationmanager =
+ protected NavigationManager(@NonNull CarContext carContext,
+ @NonNull HostDispatcher hostDispatcher) {
+ mCarContext = carContext;
+ mHostDispatcher = requireNonNull(hostDispatcher);
+ mNavigationManager =
new INavigationManager.Stub() {
@Override
- public void stopNavigation(IOnDoneCallback callback) {
+ public void onStopNavigation(IOnDoneCallback callback) {
RemoteUtils.dispatchHostCall(
- NavigationManager.this::stopNavigation, callback, "stopNavigation");
+ NavigationManager.this::onStopNavigation, callback,
+ "onStopNavigation");
}
};
}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManagerListener.java b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManagerListener.java
index ee4c29f..fa2475c 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManagerListener.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/NavigationManagerListener.java
@@ -16,12 +16,11 @@
package androidx.car.app.navigation;
-import android.annotation.SuppressLint;
-
+import androidx.car.app.CarContext;
import androidx.car.app.navigation.model.Trip;
/**
- * Listener of events from the {@link NavigationManager}.
+ * Listener for events from the {@link NavigationManager}.
*
* @see NavigationManager
*/
@@ -34,18 +33,13 @@
* guidance, routing-related notifications, and updating trip information via {@link
* NavigationManager#updateTrip(Trip)}.
*/
-
- // TODO(rampara): Listener method names must follow the on<Something> style. Consider
- // onShouldStopNavigation.
- @SuppressLint("CallbackMethodName")
- void stopNavigation();
+ void onStopNavigation();
/**
* Notifies the app that, from this point onwards, when the user chooses to navigate to a
* destination, the app should start simulating a drive towards that destination.
*
- * <p>This mode should remain active until {@link
- * androidx.car.app.CarAppService#onCarAppFinished} is called.
+ * <p>This mode should remain active until {@link CarContext#finishCarApp()} is called.
*
* <p>This functionality is used to allow verifying the app's navigation capabilities without
* being in an actual car.
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java
index 48927a8..3a267185 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/PlaceListNavigationTemplate.java
@@ -58,7 +58,7 @@
*
* <ul>
* <li>The template title has not changed, and
- * <li>The previous template is in a loading state (see {@link Builder#setIsLoading}, or the
+ * <li>The previous template is in a loading state (see {@link Builder#setLoading}, or the
* number of rows and the string contents (title, texts, not counting spans) of each row
* between the previous and new {@link ItemList}s have not changed.
* </ul>
@@ -191,10 +191,8 @@
* once the data is ready. If set to {@code false}, the UI shows the {@link ItemList}
* contents added via {@link #setItemList}.
*/
- // TODO(rampara): Consider renaming to setLoading()
- @SuppressWarnings("MissingGetterMatchingBuilder")
@NonNull
- public Builder setIsLoading(boolean isLoading) {
+ public Builder setLoading(boolean isLoading) {
this.mIsLoading = isLoading;
return this;
}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java
index f64fa55..9c0160b 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplate.java
@@ -68,7 +68,7 @@
*
* <ul>
* <li>The template title has not changed, and
- * <li>The previous template is in a loading state (see {@link Builder#setIsLoading}, or the
+ * <li>The previous template is in a loading state (see {@link Builder#setLoading}, or the
* number of rows and the string contents (title, texts, not counting spans) of each row
* between the previous and new {@link ItemList}s have not changed.
* </ul>
@@ -218,10 +218,8 @@
* once the data is ready. If set to {@code false}, the UI shows the {@link ItemList}
* contents added via {@link #setItemList}.
*/
- // TODO(rampara): Consider renaming to setLoading()
- @SuppressWarnings("MissingGetterMatchingBuilder")
@NonNull
- public Builder setIsLoading(boolean isLoading) {
+ public Builder setLoading(boolean isLoading) {
this.mIsLoading = isLoading;
return this;
}
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutingInfo.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutingInfo.java
index 1de529f..c5c2e67 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutingInfo.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/RoutingInfo.java
@@ -214,10 +214,8 @@
*
* @see #build
*/
- // TODO(rampara): Consider renaming to setLoading()
- @SuppressWarnings("MissingGetterMatchingBuilder")
@NonNull
- public Builder setIsLoading(boolean isLoading) {
+ public Builder setLoading(boolean isLoading) {
this.mIsLoading = isLoading;
return this;
}
@@ -228,7 +226,7 @@
* <h4>Requirements</h4>
*
* The {@link RoutingInfo} can be in a loading state by passing {@code true} to {@link
- * #setIsLoading(boolean)}, in which case no other fields may be set. Otherwise, the current
+ * #setLoading(boolean)}, in which case no other fields may be set. Otherwise, the current
* step and distance must be set. If the lane information is set with {@link
* Step.Builder#addLane(Lane)}, then the lane image must also be set with {@link
* Step.Builder#setLanesImage(CarIcon)}.
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/Trip.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/Trip.java
index d3f7b5d..15615f2 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/Trip.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/Trip.java
@@ -267,10 +267,8 @@
* <p>If set to {@code true}, the UI may show a loading indicator, and adding any steps
* or step travel estimates will throw an {@link IllegalArgumentException}.
*/
- // TODO(rampara): Consider renaming to setLoading()
- @SuppressWarnings("MissingGetterMatchingBuilder")
@NonNull
- public Builder setIsLoading(boolean isLoading) {
+ public Builder setLoading(boolean isLoading) {
this.mIsLoading = isLoading;
return this;
}
diff --git a/car/app/app/src/main/java/androidx/car/app/notification/CarAppExtender.java b/car/app/app/src/main/java/androidx/car/app/notification/CarAppExtender.java
index 168a094..dc259cc 100644
--- a/car/app/app/src/main/java/androidx/car/app/notification/CarAppExtender.java
+++ b/car/app/app/src/main/java/androidx/car/app/notification/CarAppExtender.java
@@ -278,7 +278,8 @@
*
* @see Builder#setSmallIcon(int)
*/
- public int getSmallIconResId() {
+ @DrawableRes
+ public int getSmallIcon() {
return mSmallIconResId;
}
@@ -288,7 +289,7 @@
* @see Builder#setLargeIcon(Bitmap)
*/
@Nullable
- public Bitmap getLargeIconBitmap() {
+ public Bitmap getLargeIcon() {
return mLargeIconBitmap;
}
@@ -391,8 +392,6 @@
* <p>This method is equivalent to {@link NotificationCompat.Builder#setSmallIcon(int)} for
* the car screen.
*/
- // TODO(rampara): Revisit small icon getter API
- @SuppressWarnings("MissingGetterMatchingBuilder")
@NonNull
public Builder setSmallIcon(int iconResId) {
this.mSmallIconResId = iconResId;
@@ -412,8 +411,6 @@
* <p>The large icon will be shown in the notification badge. If the large icon is not
* set in the {@link CarAppExtender} or the notification, the small icon will show instead.
*/
- // TODO(rampara): Revisit small icon getter API
- @SuppressWarnings("MissingGetterMatchingBuilder")
@NonNull
public Builder setLargeIcon(@Nullable Bitmap bitmap) {
this.mLargeIconBitmap = bitmap;
diff --git a/car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl b/car/app/app/src/main/java/androidx/car/app/versioning/CarAppApiLevel.java
similarity index 64%
copy from car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl
copy to car/app/app/src/main/java/androidx/car/app/versioning/CarAppApiLevel.java
index 2896a83..0da9df6 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl
+++ b/car/app/app/src/main/java/androidx/car/app/versioning/CarAppApiLevel.java
@@ -14,11 +14,17 @@
* limitations under the License.
*/
-package androidx.car.app;
+package androidx.car.app.versioning;
-import androidx.car.app.IOnDoneCallback;
+import androidx.annotation.IntDef;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
/** @hide */
-oneway interface IOnSelectedListener {
- void onSelected(int index, IOnDoneCallback callback) = 1;
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@IntDef(value = {CarAppApiLevels.LEVEL_1})
+@Retention(RetentionPolicy.SOURCE)
+public @interface CarAppApiLevel {
}
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
new file mode 100644
index 0000000..21c453b
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/versioning/CarAppApiLevels.java
@@ -0,0 +1,73 @@
+/*
+ * 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.car.app.versioning;
+
+import androidx.annotation.RestrictTo;
+import androidx.car.app.CarContext;
+
+/**
+ * API levels supported by this library.
+ * <p>
+ * Each level denotes a set of elements (classes, fields and methods) known to both clients and
+ * hosts.
+ *
+ * @see CarContext#getCarAppApiLevel()
+ */
+public class CarAppApiLevels {
+
+ /**
+ * Initial API level.
+ * <p>
+ * Includes core API services and managers, and templates for parking,
+ * charging, and navigation apps.
+ */
+ @CarAppApiLevel
+ public static final int LEVEL_1 = 1;
+
+ /**
+ * Lowest API level implement to this library
+ */
+ @CarAppApiLevel
+ public static final int OLDEST = LEVEL_1;
+
+ /**
+ * Highest API level implemented by this library.
+ */
+ @CarAppApiLevel
+ public static final int LATEST = LEVEL_1;
+
+ /**
+ * Unknown API level. Used when the API level hasn't been established yet
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @CarAppApiLevel
+ public static final int UNKNOWN = 0;
+
+ /**
+ * @return true if the given integer is a valid {@link CarAppApiLevel}
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public static boolean isValid(int carApiLevel) {
+ return carApiLevel >= OLDEST && carApiLevel <= LATEST;
+ }
+
+ private CarAppApiLevels() {}
+}
diff --git a/car/app/app/src/test/java/androidx/car/app/AppInfoTest.java b/car/app/app/src/test/java/androidx/car/app/AppInfoTest.java
new file mode 100644
index 0000000..8b6f797
--- /dev/null
+++ b/car/app/app/src/test/java/androidx/car/app/AppInfoTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.car.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+
+import androidx.car.app.versioning.CarAppApiLevels;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class AppInfoTest {
+ @Mock
+ private Context mContext;
+ @Mock
+ private PackageManager mPackageManager;
+ private final ApplicationInfo mApplicationInfo = new ApplicationInfo();
+
+ @Before
+ public void setUp() throws PackageManager.NameNotFoundException {
+ MockitoAnnotations.initMocks(this);
+
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ when(mPackageManager.getApplicationInfo(isNull(), anyInt()))
+ .thenReturn(mApplicationInfo);
+ }
+
+ @Test
+ public void create_minApiLevel_defaultsToCurrent() {
+ mApplicationInfo.metaData = null;
+ AppInfo appInfo = AppInfo.create(mContext);
+ assertThat(appInfo.getMinCarAppApiLevel()).isEqualTo(CarAppApiLevels.LATEST);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void create_minApiLevel_cannotBeLowerThanOldest() {
+ int minApiLevel = CarAppApiLevels.OLDEST - 1;
+ mApplicationInfo.metaData = new Bundle();
+ mApplicationInfo.metaData.putInt(AppInfo.MIN_API_LEVEL_MANIFEST_KEY, minApiLevel);
+ AppInfo.create(mContext);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void create_minApiLevel_cannotBeHigherThanLatest() {
+ int minApiLevel = CarAppApiLevels.LATEST + 1;
+ mApplicationInfo.metaData = new Bundle();
+ mApplicationInfo.metaData.putInt(AppInfo.MIN_API_LEVEL_MANIFEST_KEY, minApiLevel);
+ AppInfo.create(mContext);
+ }
+
+ @Test
+ public void retrieveMinApiLevel_isReadFromManifest() {
+ int minApiLevel = 123;
+ mApplicationInfo.metaData = new Bundle();
+ mApplicationInfo.metaData.putInt(AppInfo.MIN_API_LEVEL_MANIFEST_KEY, minApiLevel);
+ assertThat(AppInfo.retrieveMinCarAppApiLevel(mContext)).isEqualTo(minApiLevel);
+ }
+}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/AppManagerTest.java b/car/app/app/src/test/java/androidx/car/app/AppManagerTest.java
similarity index 95%
rename from car/app/app/src/androidTest/java/androidx/car/app/AppManagerTest.java
rename to car/app/app/src/test/java/androidx/car/app/AppManagerTest.java
index 6dd5a7f..57e4c35 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/AppManagerTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/AppManagerTest.java
@@ -30,20 +30,19 @@
import androidx.annotation.Nullable;
import androidx.car.app.model.Template;
import androidx.car.app.testing.TestCarContext;
-import androidx.test.annotation.UiThreadTest;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link AppManager}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public final class AppManagerTest {
@Mock
private ICarHost mMockCarHost;
@@ -58,7 +57,6 @@
private AppManager mAppManager;
@Before
- @UiThreadTest
public void setUp() throws RemoteException {
MockitoAnnotations.initMocks(this);
@@ -92,7 +90,6 @@
}
@Test
- @UiThreadTest
public void getTemplate_serializationFails_throwsIllegalStateException()
throws RemoteException {
mTestCarContext
@@ -113,7 +110,6 @@
}
@Test
- @UiThreadTest
public void invalidate_forwardsRequestToHost() throws RemoteException {
mAppManager.invalidate();
@@ -121,7 +117,6 @@
}
@Test
- @UiThreadTest
public void invalidate_hostThrowsRemoteException_throwsHostException() throws
RemoteException {
doThrow(new RemoteException()).when(mMockAppHost).invalidate();
@@ -130,7 +125,6 @@
}
@Test
- @UiThreadTest
public void invalidate_hostThrowsRuntimeException_throwsHostException() throws
RemoteException {
doThrow(new IllegalStateException()).when(mMockAppHost).invalidate();
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/CarAppServiceTest.java b/car/app/app/src/test/java/androidx/car/app/CarAppServiceTest.java
similarity index 63%
rename from car/app/app/src/androidTest/java/androidx/car/app/CarAppServiceTest.java
rename to car/app/app/src/test/java/androidx/car/app/CarAppServiceTest.java
index f442e0d..d5a80dd 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/CarAppServiceTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/CarAppServiceTest.java
@@ -35,25 +35,27 @@
import androidx.car.app.serialization.BundlerException;
import androidx.car.app.testing.CarAppServiceController;
import androidx.car.app.testing.TestCarContext;
+import androidx.car.app.versioning.CarAppApiLevels;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.Lifecycle;
-import androidx.test.annotation.UiThreadTest;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
import java.util.Deque;
import java.util.Locale;
/** Tests for {@link CarAppService}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public final class CarAppServiceTest {
@Mock
ICarHost mMockCarHost;
@@ -68,36 +70,24 @@
.build();
private CarAppService mCarAppService;
-
+ private CarAppServiceController mCarAppServiceController;
private Intent mIntentSet;
- private boolean mHasCarAppFinished;
+ @Captor
+ ArgumentCaptor<Bundleable> mBundleableArgumentCaptor;
@Before
- @UiThreadTest
public void setUp() {
MockitoAnnotations.initMocks(this);
-
mCarContext = TestCarContext.createCarContext(
ApplicationProvider.getApplicationContext());
-
mCarAppService =
new CarAppService() {
@Override
@NonNull
- public Screen onCreateScreen(@NonNull Intent intent) {
- mIntentSet = intent;
- return new Screen(getCarContext()) {
- @Override
- @NonNull
- public Template onGetTemplate() {
- return mTemplate;
- }
- };
- }
-
- @Override
- public void onCarAppFinished() {
- mHasCarAppFinished = true;
+ public Session onCreateSession() {
+ Session testSession = createTestSession();
+ CarAppServiceController.of(mCarContext, testSession, mCarAppService);
+ return testSession;
}
@Override
@@ -106,18 +96,36 @@
}
};
- CarAppServiceController.of(mCarContext, mCarAppService);
mCarAppService.onCreate();
+ mCarAppServiceController = CarAppServiceController.of(mCarContext, createTestSession(),
+ mCarAppService);
+ }
+
+ private Session createTestSession() {
+ return new Session() {
+ @NonNull
+ @Override
+ public Screen onCreateScreen(@NonNull Intent intent) {
+ mIntentSet = intent;
+ return new Screen(getCarContext()) {
+ @Override
+ @NonNull
+ public Template onGetTemplate() {
+ return mTemplate;
+ }
+ };
+ }
+ };
}
@Test
- @UiThreadTest
public void onAppCreate_createsFirstScreen() throws RemoteException {
ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
assertThat(
mCarAppService
+ .getCurrentSession()
.getCarContext()
.getCarService(ScreenManager.class)
.getTopTemplate()
@@ -126,7 +134,6 @@
}
@Test
- @UiThreadTest
public void onAppCreate_withIntent_callsWithOnCreateScreenWithIntent() throws
RemoteException {
IOnDoneCallback callback = mock(IOnDoneCallback.class);
@@ -139,7 +146,6 @@
}
@Test
- @UiThreadTest
public void onAppCreate_alreadyPreviouslyCreated_callsOnNewIntent() throws RemoteException {
IOnDoneCallback callback = mock(IOnDoneCallback.class);
@@ -157,7 +163,6 @@
}
@Test
- @UiThreadTest
public void onAppCreate_updatesTheConfiguration() throws RemoteException {
Configuration configuration = new Configuration();
configuration.setToDefaults();
@@ -171,7 +176,6 @@
}
@Test
- @UiThreadTest
public void onNewIntent_callsOnNewIntentWithIntent() throws RemoteException {
ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
Intent intent = new Intent("Foo");
@@ -188,12 +192,11 @@
ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
- assertThat(
- mCarAppService.getCarContext().getCarService(NavigationManager.class)).isNotNull();
+ assertThat(mCarAppService.getCurrentSession().getCarContext().getCarService(
+ NavigationManager.class)).isNotNull();
}
@Test
- @UiThreadTest
public void onConfigurationChanged_updatesTheConfiguration() throws RemoteException {
ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
@@ -209,61 +212,119 @@
}
@Test
+ public void getAppInfo() throws RemoteException, BundlerException {
+ AppInfo appInfo = new AppInfo(3, 4, "foo");
+ mCarAppServiceController.setAppInfo(appInfo);
+ ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+ IOnDoneCallback callback = mock(IOnDoneCallback.class);
+
+ carApp.getAppInfo(callback);
+
+ verify(callback).onSuccess(mBundleableArgumentCaptor.capture());
+ AppInfo receivedAppInfo = (AppInfo) mBundleableArgumentCaptor.getValue().get();
+ assertThat(receivedAppInfo.getMinCarAppApiLevel())
+ .isEqualTo(appInfo.getMinCarAppApiLevel());
+ assertThat(receivedAppInfo.getLatestCarAppApiLevel())
+ .isEqualTo(appInfo.getLatestCarAppApiLevel());
+ assertThat(receivedAppInfo.getLibraryVersion()).isEqualTo(appInfo.getLibraryVersion());
+ }
+
+ @Test
public void onHandshakeCompleted_updatesHostInfo() throws RemoteException, BundlerException {
String hostPackageName = "com.google.projection.gearhead";
ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
- HandshakeInfo handshakeInfo = new HandshakeInfo(hostPackageName);
- carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mock(IOnDoneCallback
- .class));
+ HandshakeInfo handshakeInfo = new HandshakeInfo(hostPackageName, CarAppApiLevels.LEVEL_1);
+
+ carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mock(IOnDoneCallback.class));
+
assertThat(mCarAppService.getHostInfo().getPackageName()).isEqualTo(hostPackageName);
}
@Test
- @UiThreadTest
- public void onUnbind_movesLifecycleStateToStopped() throws RemoteException {
+ public void onHandshakeCompleted_updatesCarApiLevel() throws RemoteException, BundlerException {
+ String hostPackageName = "com.google.projection.gearhead";
+ ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+ int hostApiLevel = CarAppApiLevels.LEVEL_1;
+ HandshakeInfo handshakeInfo = new HandshakeInfo(hostPackageName, hostApiLevel);
+
+ carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mock(IOnDoneCallback.class));
+
+ assertThat(
+ mCarAppService.getCurrentSession().getCarContext().getCarAppApiLevel()).isEqualTo(
+ hostApiLevel);
+ }
+
+ @Test
+ public void onHandshakeCompleted_lowerThanMinApiLevel_throws() throws BundlerException,
+ RemoteException {
+ AppInfo appInfo = new AppInfo(3, 4, "foo");
+ mCarAppServiceController.setAppInfo(appInfo);
+ ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+
+ HandshakeInfo handshakeInfo = new HandshakeInfo("bar",
+ appInfo.getMinCarAppApiLevel() - 1);
+ IOnDoneCallback callback = mock(IOnDoneCallback.class);
+ carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), callback);
+
+ verify(callback).onFailure(any());
+ }
+
+ @Test
+ public void onHandshakeCompleted_higherThanCurrentApiLevel_throws() throws BundlerException,
+ RemoteException {
+ AppInfo appInfo = new AppInfo(3, 4, "foo");
+ mCarAppServiceController.setAppInfo(appInfo);
+ ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+
+ HandshakeInfo handshakeInfo = new HandshakeInfo("bar",
+ appInfo.getLatestCarAppApiLevel() + 1);
+ IOnDoneCallback callback = mock(IOnDoneCallback.class);
+ carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), callback);
+
+ verify(callback).onFailure(any());
+ }
+
+ @Test
+ public void onUnbind_movesLifecycleStateToDestroyed() throws RemoteException {
ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
carApp.onAppStart(mock(IOnDoneCallback.class));
- mCarAppService.getLifecycle().addObserver(mLifecycleObserver);
+ mCarAppService.getCurrentSession().getLifecycle().addObserver(mLifecycleObserver);
assertThat(mCarAppService.onUnbind(null)).isTrue();
- verify(mLifecycleObserver).onStop(any());
+ verify(mLifecycleObserver).onDestroy(any());
}
@Test
- @UiThreadTest
public void onUnbind_rebind_callsOnCreateScreen() throws RemoteException {
ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
carApp.onAppStart(mock(IOnDoneCallback.class));
- mCarAppService.getLifecycle().addObserver(mLifecycleObserver);
-
+ Session currentSession = mCarAppService.getCurrentSession();
+ currentSession.getLifecycle().addObserver(mLifecycleObserver);
assertThat(mCarAppService.onUnbind(null)).isTrue();
- assertThat(mHasCarAppFinished).isTrue();
verify(mLifecycleObserver).onStop(any());
- assertThat(
- mCarAppService.getCarContext().getCarService(ScreenManager.class).getScreenStack())
- .isEmpty();
+ assertThat(currentSession.getCarContext().getCarService(
+ ScreenManager.class).getScreenStack()).isEmpty();
carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
- assertThat(
- mCarAppService.getCarContext().getCarService(ScreenManager.class).getScreenStack())
- .hasSize(1);
+ assertThat(currentSession.getCarContext().getCarService(
+ ScreenManager.class).getScreenStack()).hasSize(1);
}
@Test
- @UiThreadTest
public void onUnbind_clearsScreenStack() throws RemoteException {
ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
Deque<Screen> screenStack =
- mCarAppService.getCarContext().getCarService(ScreenManager.class).getScreenStack();
+ mCarAppService.getCurrentSession().getCarContext().getCarService(
+ ScreenManager.class).getScreenStack();
assertThat(screenStack).hasSize(1);
Screen screen = screenStack.getFirst();
@@ -273,16 +334,14 @@
assertThat(screenStack).isEmpty();
assertThat(screen.getLifecycle().getCurrentState()).isEqualTo(Lifecycle.State.DESTROYED);
- assertThat(mHasCarAppFinished).isTrue();
}
@Test
- @UiThreadTest
public void finish() throws RemoteException {
ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
- mCarAppService.finish();
+ mCarAppService.getCurrentSession().getCarContext().finishCarApp();
assertThat(mCarContext.hasCalledFinishCarApp()).isTrue();
}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/CarContextTest.java b/car/app/app/src/test/java/androidx/car/app/CarContextTest.java
similarity index 73%
rename from car/app/app/src/androidTest/java/androidx/car/app/CarContextTest.java
rename to car/app/app/src/test/java/androidx/car/app/CarContextTest.java
index ba52f42..3db83de 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/CarContextTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/CarContextTest.java
@@ -21,6 +21,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -35,31 +36,31 @@
import android.os.RemoteException;
import android.util.DisplayMetrics;
+import androidx.activity.OnBackPressedCallback;
import androidx.annotation.Nullable;
import androidx.car.app.navigation.NavigationManager;
-import androidx.car.app.test.R;
import androidx.car.app.testing.TestLifecycleOwner;
import androidx.lifecycle.Lifecycle.Event;
-import androidx.test.annotation.UiThreadTest;
+import androidx.lifecycle.Lifecycle.State;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
import java.util.Locale;
/** Tests for {@link CarContext}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class CarContextTest {
- private static final String APP_SERVICE = "app_manager";
- private static final String NAVIGATION_SERVICE = "navigation_manager";
- private static final String SCREEN_MANAGER_SERVICE = "screen_manager";
+ private static final String APP_SERVICE = "app";
+ private static final String NAVIGATION_SERVICE = "navigation";
+ private static final String SCREEN_SERVICE = "screen";
@Mock
private ICarHost mMockCarHost;
@@ -79,7 +80,6 @@
new TestLifecycleOwner();
@Before
- @UiThreadTest
public void setUp() throws RemoteException {
MockitoAnnotations.initMocks(this);
when(mMockCarHost.getHost(CarContext.APP_SERVICE))
@@ -101,7 +101,7 @@
TestStartCarAppStub startCarAppStub = new TestStartCarAppStub(mMockStartCarApp);
Bundle extras = new Bundle(1);
- extras.putBinder(CarContext.START_CAR_APP_BINDER_KEY, startCarAppStub.asBinder());
+ extras.putBinder(CarContext.EXTRA_START_CAR_APP_BINDER_KEY, startCarAppStub.asBinder());
mIntentFromNotification = new Intent().putExtras(extras);
mCarContext = CarContext.create(mLifecycleOwner.mRegistry);
@@ -130,9 +130,9 @@
@Test
public void getCarService_screenManager() {
- assertThat(mCarContext.getCarService(CarContext.SCREEN_MANAGER_SERVICE))
+ assertThat(mCarContext.getCarService(CarContext.SCREEN_SERVICE))
.isEqualTo(mCarContext.getCarService(ScreenManager.class));
- assertThat(mCarContext.getCarService(CarContext.SCREEN_MANAGER_SERVICE)).isNotNull();
+ assertThat(mCarContext.getCarService(CarContext.SCREEN_SERVICE)).isNotNull();
}
@Test
@@ -161,7 +161,7 @@
@Test
public void getCarServiceName_screenManager() {
assertThat(mCarContext.getCarServiceName(ScreenManager.class)).isEqualTo(
- SCREEN_MANAGER_SERVICE);
+ SCREEN_SERVICE);
}
@Test
@@ -210,7 +210,6 @@
}
@Test
- @UiThreadTest
public void onConfigurationChanged_updatesTheConfiguration() {
Configuration configuration = new Configuration();
configuration.setToDefaults();
@@ -223,14 +222,13 @@
}
@Test
- @UiThreadTest
public void onConfigurationChanged_loadsCorrectNewResource() {
Configuration ldpiConfig = new Configuration(mCarContext.getResources().getConfiguration());
ldpiConfig.densityDpi = 120;
mCarContext.onCarConfigurationChanged(ldpiConfig);
- Drawable ldpiDrawable = mCarContext.getDrawable(R.drawable.banana);
+ Drawable ldpiDrawable = TestUtils.getTestDrawable(mCarContext, "banana");
assertThat(ldpiDrawable.getIntrinsicHeight()).isEqualTo(48);
Configuration mdpiConfig = new Configuration(mCarContext.getResources().getConfiguration());
@@ -238,7 +236,7 @@
mCarContext.onCarConfigurationChanged(mdpiConfig);
- Drawable mdpiDrawable = mCarContext.getDrawable(R.drawable.banana);
+ Drawable mdpiDrawable = TestUtils.getTestDrawable(mCarContext, "banana");
assertThat(mdpiDrawable.getIntrinsicHeight()).isEqualTo(64);
Configuration hdpiConfig = new Configuration(mCarContext.getResources().getConfiguration());
@@ -246,12 +244,11 @@
mCarContext.onCarConfigurationChanged(hdpiConfig);
- Drawable hdpiDrawable = mCarContext.getDrawable(R.drawable.banana);
+ Drawable hdpiDrawable = TestUtils.getTestDrawable(mCarContext, "banana");
assertThat(hdpiDrawable.getIntrinsicHeight()).isEqualTo(96);
}
@Test
- @UiThreadTest
// TODO(rampara): Investigate removing usage of deprecated updateConfiguration API
@SuppressWarnings("deprecation")
public void changingApplicationContextConfiguration_doesNotChangeTheCarContextConfiguration() {
@@ -260,7 +257,7 @@
mCarContext.onCarConfigurationChanged(ldpiConfig);
- Drawable ldpiDrawable = mCarContext.getDrawable(R.drawable.banana);
+ Drawable ldpiDrawable = TestUtils.getTestDrawable(mCarContext, "banana");
assertThat(ldpiDrawable.getIntrinsicHeight()).isEqualTo(48);
Configuration mdpiConfig = new Configuration(mCarContext.getResources().getConfiguration());
@@ -272,15 +269,15 @@
.updateConfiguration(mdpiConfig,
applicationContext.getResources().getDisplayMetrics());
- Drawable carContextDrawable = mCarContext.getDrawable(R.drawable.banana);
+ Drawable carContextDrawable = TestUtils.getTestDrawable(mCarContext, "banana");
assertThat(carContextDrawable.getIntrinsicHeight()).isEqualTo(48);
- Drawable applicationContextDrawable = applicationContext.getDrawable(R.drawable.banana);
+ Drawable applicationContextDrawable = TestUtils.getTestDrawable(applicationContext,
+ "banana");
assertThat(applicationContextDrawable.getIntrinsicHeight()).isEqualTo(64);
}
@Test
- @UiThreadTest
// TODO(rampara): Investigate removing usage of deprecated updateConfiguration API
@SuppressWarnings("deprecation")
public void changingApplicationContextDisplayMetrics_doesNotChangeCarContextDisplayMetrics() {
@@ -289,7 +286,7 @@
mCarContext.onCarConfigurationChanged(ldpiConfig);
- Drawable ldpiDrawable = mCarContext.getDrawable(R.drawable.banana);
+ Drawable ldpiDrawable = TestUtils.getTestDrawable(mCarContext, "banana");
assertThat(ldpiDrawable.getIntrinsicHeight()).isEqualTo(48);
Configuration mdpiConfig = new Configuration(mCarContext.getResources().getConfiguration());
@@ -311,9 +308,8 @@
applicationContext.getResources().updateConfiguration(mdpiConfig, newDisplayMetrics);
assertThat(applicationContext.getResources().getConfiguration()).isEqualTo(mdpiConfig);
- // TODO(rampara): Investigate why DisplayMetrics isn't updated
-// assertThat(applicationContext.getResources().getDisplayMetrics()).isEqualTo(
-// newDisplayMetrics);
+ assertThat(applicationContext.getResources().getDisplayMetrics()).isEqualTo(
+ newDisplayMetrics);
assertThat(mCarContext.getResources().getConfiguration()).isNotEqualTo(mdpiConfig);
assertThat(mCarContext.getResources().getDisplayMetrics()).isNotEqualTo(newDisplayMetrics);
@@ -352,60 +348,58 @@
verify(mMockScreen2).dispatchLifecycleEvent(Event.ON_DESTROY);
}
- // TODO(rampara): Investigate how to mock final methods
-// @Test
-// public void
-// getOnBackPressedDispatcher_withAListenerThatIsStarted_callsTheListenerAndDoesNotPop() {
-// mCarContext.getCarService(ScreenManager.class).push(mScreen1);
-// mCarContext.getCarService(ScreenManager.class).push(mScreen2);
-//
-// OnBackPressedCallback callback = mock(OnBackPressedCallback.class);
-// when(callback.isEnabled()).thenReturn(true);
-// mLifecycleOwner.mRegistry.setCurrentState(State.STARTED);
-//
-// mCarContext.getOnBackPressedDispatcher().addCallback(mLifecycleOwner, callback);
-// mCarContext.getOnBackPressedDispatcher().onBackPressed();
-//
-// verify(callback).handleOnBackPressed();
-// verify(mMockScreen1, never()).dispatchLifecycleEvent(Event.ON_DESTROY);
-// verify(mMockScreen2, never()).dispatchLifecycleEvent(Event.ON_DESTROY);
-// }
-//
-// @Test
-// public void getOnBackPressedDispatcher_withAListenerThatIsNotStarted_popsAScreen() {
-// mCarContext.getCarService(ScreenManager.class).push(mScreen1);
-// mCarContext.getCarService(ScreenManager.class).push(mScreen2);
-//
-// OnBackPressedCallback callback = mock(OnBackPressedCallback.class);
-// when(callback.isEnabled()).thenReturn(true);
-// mLifecycleOwner.mRegistry.setCurrentState(State.CREATED);
-//
-// mCarContext.getOnBackPressedDispatcher().addCallback(mLifecycleOwner, callback);
-// mCarContext.getOnBackPressedDispatcher().onBackPressed();
-//
-// verify(callback, never()).handleOnBackPressed();
-// verify(mMockScreen1, never()).dispatchLifecycleEvent(Lifecycle.Event.ON_DESTROY);
-// verify(mMockScreen2).dispatchLifecycleEvent(Lifecycle.Event.ON_DESTROY);
-// }
-//
-// @Test
-// public void getOnBackPressedDispatcher_callsDefaultListenerWheneverTheAddedOneIsNotSTARTED() {
-// mCarContext.getCarService(ScreenManager.class).push(mScreen1);
-// mCarContext.getCarService(ScreenManager.class).push(mScreen2);
-//
-// OnBackPressedCallback callback = mock(OnBackPressedCallback.class);
-// when(callback.isEnabled()).thenReturn(true);
-// mLifecycleOwner.mRegistry.setCurrentState(State.CREATED);
-//
-// mCarContext.getOnBackPressedDispatcher().addCallback(mLifecycleOwner, callback);
-// mCarContext.getOnBackPressedDispatcher().onBackPressed();
-//
-// verify(callback, never()).handleOnBackPressed();
-// verify(mMockScreen2).dispatchLifecycleEvent(Event.ON_DESTROY);
-//
-// mLifecycleOwner.mRegistry.setCurrentState(State.STARTED);
-// mCarContext.getOnBackPressedDispatcher().onBackPressed();
-//
-// verify(callback).handleOnBackPressed();
-// }
+ @Test
+ public void getOnBackPressedDispatcher_withListenerThatIsStarted_callsListenerAndDoesNotPop() {
+ mCarContext.getCarService(ScreenManager.class).push(mScreen1);
+ mCarContext.getCarService(ScreenManager.class).push(mScreen2);
+
+ OnBackPressedCallback callback = mock(OnBackPressedCallback.class);
+ when(callback.isEnabled()).thenReturn(true);
+ mLifecycleOwner.mRegistry.setCurrentState(State.STARTED);
+
+ mCarContext.getOnBackPressedDispatcher().addCallback(mLifecycleOwner, callback);
+ mCarContext.getOnBackPressedDispatcher().onBackPressed();
+
+ verify(callback).handleOnBackPressed();
+ verify(mMockScreen1, never()).dispatchLifecycleEvent(Event.ON_DESTROY);
+ verify(mMockScreen2, never()).dispatchLifecycleEvent(Event.ON_DESTROY);
+ }
+
+ @Test
+ public void getOnBackPressedDispatcher_withAListenerThatIsNotStarted_popsAScreen() {
+ mCarContext.getCarService(ScreenManager.class).push(mScreen1);
+ mCarContext.getCarService(ScreenManager.class).push(mScreen2);
+
+ OnBackPressedCallback callback = mock(OnBackPressedCallback.class);
+ when(callback.isEnabled()).thenReturn(true);
+ mLifecycleOwner.mRegistry.setCurrentState(State.CREATED);
+
+ mCarContext.getOnBackPressedDispatcher().addCallback(mLifecycleOwner, callback);
+ mCarContext.getOnBackPressedDispatcher().onBackPressed();
+
+ verify(callback, never()).handleOnBackPressed();
+ verify(mMockScreen1, never()).dispatchLifecycleEvent(Event.ON_DESTROY);
+ verify(mMockScreen2).dispatchLifecycleEvent(Event.ON_DESTROY);
+ }
+
+ @Test
+ public void getOnBackPressedDispatcher_callsDefaultListenerWheneverTheAddedOneIsNotSTARTED() {
+ mCarContext.getCarService(ScreenManager.class).push(mScreen1);
+ mCarContext.getCarService(ScreenManager.class).push(mScreen2);
+
+ OnBackPressedCallback callback = mock(OnBackPressedCallback.class);
+ when(callback.isEnabled()).thenReturn(true);
+ mLifecycleOwner.mRegistry.setCurrentState(State.CREATED);
+
+ mCarContext.getOnBackPressedDispatcher().addCallback(mLifecycleOwner, callback);
+ mCarContext.getOnBackPressedDispatcher().onBackPressed();
+
+ verify(callback, never()).handleOnBackPressed();
+ verify(mMockScreen2).dispatchLifecycleEvent(Event.ON_DESTROY);
+
+ mLifecycleOwner.mRegistry.setCurrentState(State.STARTED);
+ mCarContext.getOnBackPressedDispatcher().onBackPressed();
+
+ verify(callback).handleOnBackPressed();
+ }
}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/CarToastTest.java b/car/app/app/src/test/java/androidx/car/app/CarToastTest.java
similarity index 92%
rename from car/app/app/src/androidTest/java/androidx/car/app/CarToastTest.java
rename to car/app/app/src/test/java/androidx/car/app/CarToastTest.java
index 42f0e70..3ea0e15 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/CarToastTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/CarToastTest.java
@@ -22,23 +22,21 @@
import androidx.car.app.testing.TestAppManager;
import androidx.car.app.testing.TestCarContext;
-import androidx.test.annotation.UiThreadTest;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link CarToast}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public final class CarToastTest {
private TestCarContext mCarContext;
@Before
- @UiThreadTest
public void setUp() {
mCarContext = TestCarContext.createCarContext(ApplicationProvider.getApplicationContext());
mCarContext.reset();
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/HandshakeInfoTest.java b/car/app/app/src/test/java/androidx/car/app/HandshakeInfoTest.java
similarity index 77%
rename from car/app/app/src/androidTest/java/androidx/car/app/HandshakeInfoTest.java
rename to car/app/app/src/test/java/androidx/car/app/HandshakeInfoTest.java
index 40b39c8..a367e7a 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/HandshakeInfoTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/HandshakeInfoTest.java
@@ -18,20 +18,21 @@
import static com.google.common.truth.Truth.assertThat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public final class HandshakeInfoTest {
@Test
public void construct_handshakeInfo() {
String hostPackageName = "com.google.host";
- HandshakeInfo handshakeInfo = new HandshakeInfo(hostPackageName);
+ int hostApiLevel = 123;
+ HandshakeInfo handshakeInfo = new HandshakeInfo(hostPackageName, hostApiLevel);
assertThat(handshakeInfo.getHostPackageName()).isEqualTo(hostPackageName);
+ assertThat(handshakeInfo.getHostCarAppApiLevel()).isEqualTo(hostApiLevel);
}
}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/HostDispatcherTest.java b/car/app/app/src/test/java/androidx/car/app/HostDispatcherTest.java
similarity index 96%
rename from car/app/app/src/androidTest/java/androidx/car/app/HostDispatcherTest.java
rename to car/app/app/src/test/java/androidx/car/app/HostDispatcherTest.java
index 3e5bc5c..1366265 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/HostDispatcherTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/HostDispatcherTest.java
@@ -29,19 +29,19 @@
import androidx.annotation.Nullable;
import androidx.car.app.navigation.INavigationHost;
import androidx.car.app.serialization.Bundleable;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link HostDispatcher}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class HostDispatcherTest {
@Mock
@@ -55,7 +55,6 @@
private HostDispatcher mHostDispatcher = new HostDispatcher();
@Before
- @UiThreadTest
public void setUp() throws RemoteException {
MockitoAnnotations.initMocks(this);
@@ -149,7 +148,6 @@
}
@Test
- @UiThreadTest
public void getHost_afterResetting_getsFromCarHost() throws RemoteException {
assertThat(mHostDispatcher.getHost(CarContext.APP_SERVICE)).isEqualTo(mAppHost);
@@ -211,7 +209,6 @@
}
@Test
- @UiThreadTest
public void getHost_afterReset_throwsHostException() {
mHostDispatcher.resetHosts();
@@ -219,7 +216,6 @@
}
@Test
- @UiThreadTest
public void getHost_notBound_throwsHostException() {
mHostDispatcher = new HostDispatcher();
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/ScreenManagerTest.java b/car/app/app/src/test/java/androidx/car/app/ScreenManagerTest.java
similarity index 98%
rename from car/app/app/src/androidTest/java/androidx/car/app/ScreenManagerTest.java
rename to car/app/app/src/test/java/androidx/car/app/ScreenManagerTest.java
index 38432dd..4e55c08 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/ScreenManagerTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/ScreenManagerTest.java
@@ -34,10 +34,7 @@
import androidx.car.app.testing.TestLifecycleOwner;
import androidx.lifecycle.Lifecycle.Event;
import androidx.lifecycle.Lifecycle.State;
-import androidx.test.annotation.UiThreadTest;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Test;
@@ -45,10 +42,12 @@
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link ScreenManager}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public final class ScreenManagerTest {
private TestScreen mScreen1;
@@ -71,7 +70,6 @@
private ScreenManager mScreenManager;
@Before
- @UiThreadTest
public void setUp() {
MockitoAnnotations.initMocks(this);
@@ -131,7 +129,6 @@
}
@Test
- @UiThreadTest
public void push_stackHadScreen_addsToStack_callsProperLifecycleMethods() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager);
@@ -158,7 +155,6 @@
}
@Test
- @UiThreadTest
public void push_screenAlreadyInStack_movesToTop_callsProperLifecycleMethods() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager);
@@ -194,7 +190,6 @@
}
@Test
- @UiThreadTest
public void push_screenAlreadyTopOfStack_noFurtherLifecycleCalls() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager);
@@ -223,7 +218,6 @@
}
@Test
- @UiThreadTest
public void pushForResult_addsToStack_callsProperLifecycleMethods() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager,
@@ -251,7 +245,6 @@
}
@Test
- @UiThreadTest
public void pushForResult_screenSetsResult_firstScreenGetsCalled() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
@@ -269,7 +262,6 @@
}
@Test
- @UiThreadTest
public void pushForResult_screenSetsResult_firstScreenGetsCalled_callsProperLifecycleMethods() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager,
@@ -347,7 +339,6 @@
}
@Test
- @UiThreadTest
public void pop_removes_callsProperLifecycleMethods() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager);
@@ -445,7 +436,6 @@
}
@Test
- @UiThreadTest
public void popTo_pops_callsProperLifecycleMethods() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager);
@@ -486,7 +476,6 @@
}
@Test
- @UiThreadTest
public void popTo_markerScreenNotInStack_popsToRoot_callsProperLifecycleMethods() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockScreen3, mMockAppManager);
@@ -537,7 +526,6 @@
}
@Test
- @UiThreadTest
public void popTo_multipleToPop_pops_callsProperLifecycleMethods() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockScreen3, mMockAppManager);
@@ -591,7 +579,6 @@
}
@Test
- @UiThreadTest
public void popTo_notARootTarget_popsExpectedScreens_callsProperLifecycleMethods() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockScreen3, mMockAppManager);
@@ -675,7 +662,6 @@
}
@Test
- @UiThreadTest
public void popToRoot_pops_callsProperLifecycleMethods() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager);
@@ -713,7 +699,6 @@
}
@Test
- @UiThreadTest
public void popToRoot_multipleToPop_pops_callsProperLifecycleMethods() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockScreen3, mMockAppManager);
@@ -764,7 +749,6 @@
}
@Test
- @UiThreadTest
public void popTo_multipleToPop_withResult_pops_callsProperLifecycleMethods() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
InOrder inOrder =
@@ -858,7 +842,6 @@
}
@Test
- @UiThreadTest
public void remove_wasTop_removes_callsProperLifecycleMethods() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager);
@@ -897,7 +880,6 @@
}
@Test
- @UiThreadTest
public void remove_wasNotTop_removes_callsProperLifecycleMethods() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockAppManager);
@@ -930,7 +912,6 @@
}
@Test
- @UiThreadTest
public void remove_notInStack_noop() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
InOrder inOrder = inOrder(mMockScreen1, mMockScreen2, mMockScreen3, mMockAppManager);
@@ -960,7 +941,6 @@
}
@Test
- @UiThreadTest
public void getTopTemplate_returnsTemplateFromTopOfStack() {
Template template =
PlaceListMapTemplate.builder()
@@ -993,7 +973,6 @@
}
@Test
- @UiThreadTest
public void dispatchAppLifecycleEvent_onStart_expectedLifecycleChange() {
mScreenManager.push(mScreen1);
reset(mMockScreen1);
@@ -1009,7 +988,6 @@
}
@Test
- @UiThreadTest
public void dispatchAppLifecycleEvent_onResume_expectedLifecycleChange() {
mScreenManager.push(mScreen1);
reset(mMockScreen1);
@@ -1025,7 +1003,6 @@
}
@Test
- @UiThreadTest
public void dispatchAppLifecycleEvent_onPause_expectedLifecycleChange() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
mScreenManager.push(mScreen1);
@@ -1042,7 +1019,6 @@
}
@Test
- @UiThreadTest
public void dispatchAppLifecycleEvent_onStop_expectedLifecycleChange() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
mScreenManager.push(mScreen1);
@@ -1067,7 +1043,6 @@
}
@Test
- @UiThreadTest
public void dispatchAppLifecycleEvent_onDestroy_screenStopped_onlyDestroys() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
mScreenManager.push(mScreen1);
@@ -1083,7 +1058,6 @@
}
@Test
- @UiThreadTest
public void dispatchAppLifecycleEvent_onDestroy_screenPaused_stopsAndDestroys() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
mScreenManager.push(mScreen1);
@@ -1100,7 +1074,6 @@
}
@Test
- @UiThreadTest
public void dispatchAppLifecycleEvent_onDestroy_screenResumed_pausesStopsAndDestroys() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
mScreenManager.push(mScreen1);
@@ -1117,7 +1090,6 @@
}
@Test
- @UiThreadTest
public void dispatchAppLifecycleEvent_onDestroy_pausesStopsAndDestroysTop_destroysOthers() {
mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_RESUME);
mScreenManager.push(mScreen1);
@@ -1140,7 +1112,6 @@
}
@Test
- @UiThreadTest
public void pop_screenReuseLastTemplateId() {
Template template =
PlaceListMapTemplate.builder()
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/ScreenTest.java b/car/app/app/src/test/java/androidx/car/app/ScreenTest.java
similarity index 93%
rename from car/app/app/src/androidTest/java/androidx/car/app/ScreenTest.java
rename to car/app/app/src/test/java/androidx/car/app/ScreenTest.java
index 7ab1650..98e0f42 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/ScreenTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/ScreenTest.java
@@ -31,20 +31,19 @@
import androidx.car.app.testing.TestScreenManager;
import androidx.lifecycle.Lifecycle.Event;
import androidx.lifecycle.Lifecycle.State;
-import androidx.test.annotation.UiThreadTest;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link Screen}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public final class ScreenTest {
private TestCarContext mCarContext;
@@ -54,7 +53,6 @@
private Screen mScreen;
@Before
- @UiThreadTest
public void setUp() {
MockitoAnnotations.initMocks(this);
mCarContext =
@@ -87,7 +85,6 @@
}
@Test
- @UiThreadTest
public void finish_removesSelf() {
mScreen.finish();
assertThat(mCarContext.getCarService(TestScreenManager.class).getScreensRemoved())
@@ -95,14 +92,12 @@
}
@Test
- @UiThreadTest
public void onCreate_expectedLifecycleChange() {
mScreen.dispatchLifecycleEvent(Event.ON_CREATE);
assertThat(mScreen.getLifecycle().getCurrentState()).isEqualTo(State.CREATED);
}
@Test
- @UiThreadTest
public void onStart_expectedLifecycleChange() {
mScreen.dispatchLifecycleEvent(Event.ON_START);
assertThat(mScreen.getLifecycle().getCurrentState()).isEqualTo(State.STARTED);
@@ -115,28 +110,24 @@
}
@Test
- @UiThreadTest
public void onPause_expectedLifecycleChange() {
mScreen.dispatchLifecycleEvent(Event.ON_PAUSE);
assertThat(mScreen.getLifecycle().getCurrentState()).isEqualTo(State.STARTED);
}
@Test
- @UiThreadTest
public void onStop_expectedLifecycleChange() {
mScreen.dispatchLifecycleEvent(Event.ON_STOP);
assertThat(mScreen.getLifecycle().getCurrentState()).isEqualTo(State.CREATED);
}
@Test
- @UiThreadTest
public void onDestroy_expectedLifecycleChange() {
mScreen.dispatchLifecycleEvent(Event.ON_DESTROY);
assertThat(mScreen.getLifecycle().getCurrentState()).isEqualTo(State.DESTROYED);
}
@Test
- @UiThreadTest
public void setResult_callsThemockOnScreenResultCallback() {
mScreen.setOnResultCallback(mMockOnScreenResultCallback);
@@ -151,7 +142,6 @@
}
@Test
- @UiThreadTest
public void finish_screenIsDestroyed() {
mScreen.finish();
assertThat(mScreen.getLifecycle().getCurrentState()).isEqualTo(State.DESTROYED);
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/TestData.java b/car/app/app/src/test/java/androidx/car/app/TestData.java
similarity index 100%
rename from car/app/app/src/androidTest/java/androidx/car/app/TestData.java
rename to car/app/app/src/test/java/androidx/car/app/TestData.java
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/TestScreen.java b/car/app/app/src/test/java/androidx/car/app/TestScreen.java
similarity index 100%
rename from car/app/app/src/androidTest/java/androidx/car/app/TestScreen.java
rename to car/app/app/src/test/java/androidx/car/app/TestScreen.java
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/TestUtils.java b/car/app/app/src/test/java/androidx/car/app/TestUtils.java
similarity index 90%
rename from car/app/app/src/androidTest/java/androidx/car/app/TestUtils.java
rename to car/app/app/src/test/java/androidx/car/app/TestUtils.java
index d946284..7c6f6cc 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/TestUtils.java
+++ b/car/app/app/src/test/java/androidx/car/app/TestUtils.java
@@ -27,8 +27,10 @@
import android.content.Context;
import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
import android.text.SpannableString;
+import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@@ -42,6 +44,7 @@
import androidx.car.app.model.Pane;
import androidx.car.app.model.Row;
import androidx.car.app.model.SectionedItemList;
+import androidx.core.graphics.drawable.IconCompat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
@@ -59,6 +62,20 @@
private TestUtils() {
}
+ public static Drawable getTestDrawable(Context context, String drawable) {
+ return context.getDrawable(getTestDrawableResId(context, drawable));
+ }
+
+ public static CarIcon getTestCarIcon(Context context, String drawable) {
+ return CarIcon.of(IconCompat.createWithResource(context,
+ TestUtils.getTestDrawableResId(context, drawable)));
+ }
+
+ @DrawableRes
+ public static int getTestDrawableResId(Context context, String drawable) {
+ return context.getResources().getIdentifier(drawable, "drawable", context.getPackageName());
+ }
+
/**
* Returns a {@link DateTimeWithZone} instance from a date string and a time zone id.
*
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ActionStripTest.java b/car/app/app/src/test/java/androidx/car/app/model/ActionStripTest.java
similarity index 95%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/ActionStripTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/ActionStripTest.java
index b0a3a200..778db2d 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/ActionStripTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/ActionStripTest.java
@@ -20,15 +20,14 @@
import static org.junit.Assert.assertThrows;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link ActionStrip}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class ActionStripTest {
@Test
public void createEmpty_throws() {
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ActionTest.java b/car/app/app/src/test/java/androidx/car/app/model/ActionTest.java
similarity index 82%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/ActionTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/ActionTest.java
index 93a2562..1535bf9 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/ActionTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/ActionTest.java
@@ -23,16 +23,14 @@
import static org.mockito.Mockito.verify;
import android.content.ContentResolver;
+import android.content.Context;
import android.net.Uri;
import androidx.car.app.IOnDoneCallback;
import androidx.car.app.OnDoneCallback;
-import androidx.car.app.test.R;
+import androidx.car.app.TestUtils;
import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.annotation.UiThreadTest;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Rule;
import org.junit.Test;
@@ -40,10 +38,12 @@
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link Action}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class ActionTest {
@Rule
public final MockitoRule mockito = MockitoJUnit.rule();
@@ -79,23 +79,20 @@
assertThrows(
IllegalArgumentException.class,
() -> Action.builder()
- .setTitle("foo")
- .setOnClickListener(onClickListener)
- .setBackgroundColor(CarColor.createCustom(0xdead, 0xbeef))
- .build());
+ .setTitle("foo")
+ .setOnClickListener(onClickListener)
+ .setBackgroundColor(CarColor.createCustom(0xdead, 0xbeef))
+ .build());
}
@Test
public void create_noTitleDefault() {
OnClickListener onClickListener = mock(OnClickListener.class);
Action action = Action.builder()
- .setIcon(
- CarIcon.of(
- IconCompat.createWithResource(
- ApplicationProvider.getApplicationContext(),
- R.drawable.ic_test_1)))
- .setOnClickListener(onClickListener)
- .build();
+ .setIcon(TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+ "ic_test_1"))
+ .setOnClickListener(onClickListener)
+ .build();
assertThat(action.getTitle()).isNull();
}
@@ -116,19 +113,18 @@
}
@Test
- @UiThreadTest
public void createInstance() {
OnClickListener onClickListener = mock(OnClickListener.class);
- IconCompat icon =
- IconCompat.createWithResource(
- ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1);
+ Context context = ApplicationProvider.getApplicationContext();
+ IconCompat icon = IconCompat.createWithResource(
+ context, TestUtils.getTestDrawableResId(context, "ic_test_1"));
String title = "foo";
Action action = Action.builder()
- .setTitle(title)
- .setIcon(CarIcon.of(icon))
- .setBackgroundColor(CarColor.BLUE)
- .setOnClickListener(onClickListener)
- .build();
+ .setTitle(title)
+ .setIcon(CarIcon.of(icon))
+ .setBackgroundColor(CarColor.BLUE)
+ .setOnClickListener(onClickListener)
+ .build();
assertThat(icon).isEqualTo(action.getIcon().getIcon());
assertThat(CarText.create(title)).isEqualTo(action.getTitle());
assertThat(CarColor.BLUE).isEqualTo(action.getBackgroundColor());
@@ -199,9 +195,9 @@
CarIcon icon2 = CarIcon.APP_ICON;
Action action1 = Action.builder().setOnClickListener(() -> {
- }).setTitle(title).setIcon(icon1).build();
+ }).setTitle(title).setIcon(icon1).build();
Action action2 = Action.builder().setOnClickListener(() -> {
- }).setTitle(title).setIcon(icon2).build();
+ }).setTitle(title).setIcon(icon2).build();
assertThat(action2).isNotEqualTo(action1);
}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/CarIconSpanTest.java b/car/app/app/src/test/java/androidx/car/app/model/CarIconSpanTest.java
similarity index 84%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/CarIconSpanTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/CarIconSpanTest.java
index 2439237..0237996 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/CarIconSpanTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/CarIconSpanTest.java
@@ -21,29 +21,30 @@
import static org.junit.Assert.assertThrows;
import android.content.ContentResolver;
+import android.content.Context;
import android.net.Uri;
-import androidx.car.app.test.R;
+import androidx.car.app.TestUtils;
import androidx.core.graphics.drawable.IconCompat;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link CarIconSpan}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class CarIconSpanTest {
private IconCompat mIcon;
@Before
public void setup() {
- mIcon =
- IconCompat.createWithResource(
- ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1);
+ Context context = ApplicationProvider.getApplicationContext();
+ mIcon = IconCompat.createWithResource(
+ context, TestUtils.getTestDrawableResId(context, "ic_test_1"));
}
@Test
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/CarIconTest.java b/car/app/app/src/test/java/androidx/car/app/model/CarIconTest.java
similarity index 87%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/CarIconTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/CarIconTest.java
index 1152c7e..e6a33ab 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/CarIconTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/CarIconTest.java
@@ -28,32 +28,33 @@
import static org.junit.Assert.assertThrows;
import android.content.ContentResolver;
+import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
-import androidx.car.app.test.R;
+import androidx.car.app.TestUtils;
import androidx.core.graphics.drawable.IconCompat;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
import java.io.File;
/** Tests for {@link CarIcon}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class CarIconTest {
private IconCompat mIcon;
@Before
public void setup() {
- mIcon =
- IconCompat.createWithResource(
- ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1);
+ Context context = ApplicationProvider.getApplicationContext();
+ mIcon = IconCompat.createWithResource(
+ context, TestUtils.getTestDrawableResId(context, "ic_test_1"));
}
@Test
@@ -142,11 +143,10 @@
public void equals() {
assertThat(BACK.equals(BACK)).isTrue();
CarIcon carIcon = CarIcon.of(mIcon);
+ Context context = ApplicationProvider.getApplicationContext();
- assertThat(
- CarIcon.of(
- IconCompat.createWithResource(
- ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1)))
+ assertThat(CarIcon.of(IconCompat.createWithResource(
+ context, TestUtils.getTestDrawableResId(context, "ic_test_1"))))
.isEqualTo(carIcon);
}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/DateTimeWithZoneTest.java b/car/app/app/src/test/java/androidx/car/app/model/DateTimeWithZoneTest.java
similarity index 85%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/DateTimeWithZoneTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/DateTimeWithZoneTest.java
index 1844d51..43bdbf0 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/DateTimeWithZoneTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/DateTimeWithZoneTest.java
@@ -24,12 +24,10 @@
import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
import java.time.Duration;
import java.time.ZonedDateTime;
@@ -38,8 +36,8 @@
import java.util.TimeZone;
/** Tests for {@link DateTimeWithZone}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class DateTimeWithZoneTest {
@Test
@SuppressWarnings("JdkObsolete")
@@ -73,36 +71,29 @@
// Negative time.
assertThrows(
IllegalArgumentException.class,
- () -> {
- DateTimeWithZone.create(-1, (int) timeZoneOffsetSeconds, zoneShortName);
- });
+ () -> DateTimeWithZone.create(-1, (int) timeZoneOffsetSeconds, zoneShortName));
// Offset out of range.
assertThrows(
IllegalArgumentException.class,
- () -> {
- DateTimeWithZone.create(timeSinceEpochMillis, 18 * 60 * 60 + 1, zoneShortName);
- });
+ () -> DateTimeWithZone.create(timeSinceEpochMillis, 18 * 60 * 60 + 1,
+ zoneShortName));
assertThrows(
IllegalArgumentException.class,
- () -> {
- DateTimeWithZone.create(timeSinceEpochMillis, -18 * 60 * 60 - 1, zoneShortName);
- });
+ () -> DateTimeWithZone.create(timeSinceEpochMillis, -18 * 60 * 60 - 1,
+ zoneShortName));
// Null short name.
assertThrows(
NullPointerException.class,
- () -> {
- DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds,
- null);
- });
+ () -> DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds,
+ null));
// Empty short name.
assertThrows(
IllegalArgumentException.class,
- () -> {
- DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds, "");
- });
+ () -> DateTimeWithZone.create(timeSinceEpochMillis, (int) timeZoneOffsetSeconds,
+ ""));
}
@Test
@@ -131,20 +122,15 @@
// Negative time.
assertThrows(
IllegalArgumentException.class,
- () -> {
- DateTimeWithZone.create(-1, timeZone);
- });
+ () -> DateTimeWithZone.create(-1, timeZone));
// Null time zone.
assertThrows(
NullPointerException.class,
- () -> {
- DateTimeWithZone.create(123, null);
- });
+ () -> DateTimeWithZone.create(123, null));
}
@Test
- @SdkSuppress(minSdkVersion = 26)
public void create_withZonedDateTime() {
ZonedDateTime zonedDateTime = ZonedDateTime.parse("2020-05-14T19:57:00-07:00[US/Pacific]");
DateTimeWithZone dateTimeWithZone = DateTimeWithZone.create(zonedDateTime);
@@ -153,7 +139,6 @@
}
@Test
- @SdkSuppress(minSdkVersion = 26)
public void create_withZonedDateTime_argumentChecks() {
// Null date time.
assertThrows(
@@ -164,7 +149,6 @@
}
@Test
- @SdkSuppress(minSdkVersion = 26)
public void equals() {
TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
long timeSinceEpochMillis = System.currentTimeMillis();
@@ -183,7 +167,6 @@
}
@Test
- @SdkSuppress(minSdkVersion = 26)
public void notEquals_differentTimeSinceEpoch() {
TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
long timeSinceEpochMillis = System.currentTimeMillis();
@@ -203,7 +186,6 @@
}
@Test
- @SdkSuppress(minSdkVersion = 26)
public void notEquals_differentTimeZoneOffsetSeconds() {
TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
long timeSinceEpochMillis = System.currentTimeMillis();
@@ -223,7 +205,6 @@
}
@Test
- @SdkSuppress(minSdkVersion = 26)
public void notEquals_differentTimeZone() {
TimeZone timeZone = TimeZone.getTimeZone("US/Pacific");
long timeSinceEpochMillis = System.currentTimeMillis();
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/DistanceSpanTest.java b/car/app/app/src/test/java/androidx/car/app/model/DistanceSpanTest.java
similarity index 90%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/DistanceSpanTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/DistanceSpanTest.java
index a05bed7..704770b 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/DistanceSpanTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/DistanceSpanTest.java
@@ -18,15 +18,14 @@
import static com.google.common.truth.Truth.assertThat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link DistanceSpan}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class DistanceSpanTest {
private final Distance mDistance =
Distance.create(/* displayDistance= */ 10, Distance.UNIT_KILOMETERS);
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/DistanceTest.java b/car/app/app/src/test/java/androidx/car/app/model/DistanceTest.java
similarity index 93%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/DistanceTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/DistanceTest.java
index 91c7028..4ebe6fb 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/DistanceTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/DistanceTest.java
@@ -24,15 +24,14 @@
import static org.junit.Assert.assertThrows;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link Distance}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class DistanceTest {
private static final double DISPLAY_DISTANCE = 1.2d;
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/DurationSpanTest.java b/car/app/app/src/test/java/androidx/car/app/model/DurationSpanTest.java
similarity index 88%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/DurationSpanTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/DurationSpanTest.java
index e43f2e1..cce7162 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/DurationSpanTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/DurationSpanTest.java
@@ -18,15 +18,14 @@
import static com.google.common.truth.Truth.assertThat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link DurationSpan}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class DurationSpanTest {
@Test
public void constructor() {
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ForegroundCarColorSpanTest.java b/car/app/app/src/test/java/androidx/car/app/model/ForegroundCarColorSpanTest.java
similarity index 91%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/ForegroundCarColorSpanTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/ForegroundCarColorSpanTest.java
index 4f320f0..a3b92a1 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/ForegroundCarColorSpanTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/ForegroundCarColorSpanTest.java
@@ -23,15 +23,14 @@
import static org.junit.Assert.assertThrows;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link CarIconSpan}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class ForegroundCarColorSpanTest {
@Test
public void constructor() {
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/GridItemTest.java b/car/app/app/src/test/java/androidx/car/app/model/GridItemTest.java
similarity index 77%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/GridItemTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/GridItemTest.java
index 68e0a3f..3c767ed 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/GridItemTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/GridItemTest.java
@@ -28,25 +28,23 @@
import android.os.RemoteException;
import androidx.car.app.OnDoneCallback;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link GridItem}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class GridItemTest {
@Test
public void create_defaultValues() {
- GridItem gridItem = GridItem.builder().setImage(BACK).build();
+ GridItem gridItem = GridItem.builder().setTitle("Title").setImage(BACK).build();
assertThat(BACK).isEqualTo(gridItem.getImage());
assertThat(gridItem.getImageType()).isEqualTo(GridItem.IMAGE_TYPE_LARGE);
- assertThat(gridItem.getTitle()).isNull();
assertThat(gridItem.getText()).isNull();
}
@@ -59,6 +57,17 @@
}
@Test
+ public void title_throwsIfNotSet() {
+ // Not set
+ assertThrows(IllegalStateException.class, () -> GridItem.builder().setImage(BACK).build());
+
+ // Not set
+ assertThrows(
+ IllegalArgumentException.class, () -> GridItem.builder().setTitle("").setImage(
+ BACK).build());
+ }
+
+ @Test
public void text_charSequence() {
String text = "foo";
GridItem gridItem = GridItem.builder().setTitle("title").setText(text).setImage(
@@ -110,9 +119,10 @@
@Test
public void notEquals_differentImage() {
- GridItem gridItem = GridItem.builder().setImage(BACK).build();
+ GridItem gridItem = GridItem.builder().setTitle("Title").setImage(BACK).build();
- assertThat(GridItem.builder().setImage(ALERT).build()).isNotEqualTo(gridItem);
+ assertThat(GridItem.builder().setImage(ALERT).setTitle("Title").build()).isNotEqualTo(
+ gridItem);
}
@Test
@@ -121,18 +131,20 @@
}).setChecked(true).build();
Toggle toggle2 = Toggle.builder(isChecked -> {
}).setChecked(false).build();
- GridItem gridItem = GridItem.builder().setImage(BACK).setToggle(toggle1).build();
+ GridItem gridItem = GridItem.builder().setTitle("Title").setImage(BACK).setToggle(
+ toggle1).build();
- assertThat(GridItem.builder().setImage(BACK).setToggle(toggle2).build()).isNotEqualTo(
+ assertThat(GridItem.builder().setImage(BACK).setTitle("Title").setToggle(
+ toggle2).build()).isNotEqualTo(
gridItem);
}
@Test
- @UiThreadTest
public void clickListener() throws RemoteException {
OnClickListener onClickListener = mock(OnClickListener.class);
GridItem gridItem =
- GridItem.builder().setImage(BACK).setOnClickListener(onClickListener).build();
+ GridItem.builder().setTitle("Title").setImage(BACK).setOnClickListener(
+ onClickListener).build();
OnDoneCallback onDoneCallback = mock(OnDoneCallback.class);
gridItem.getOnClickListener().onClick(onDoneCallback);
verify(onClickListener).onClick();
@@ -143,7 +155,8 @@
public void setToggle() {
Toggle toggle = Toggle.builder(isChecked -> {
}).build();
- GridItem gridItem = GridItem.builder().setImage(BACK).setToggle(toggle).build();
+ GridItem gridItem =
+ GridItem.builder().setTitle("Title").setImage(BACK).setToggle(toggle).build();
assertThat(toggle).isEqualTo(gridItem.getToggle());
}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/GridTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/model/GridTemplateTest.java
similarity index 95%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/GridTemplateTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/GridTemplateTest.java
index 5548996..6078ee3 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/GridTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/GridTemplateTest.java
@@ -23,15 +23,15 @@
import static org.junit.Assert.assertThrows;
import androidx.car.app.TestUtils;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link GridTemplate}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class GridTemplateTest {
@Test
public void createInstance_emptyList_notLoading_throws() {
@@ -162,7 +162,7 @@
ItemList itemList = ItemList.builder().build();
GridTemplate template =
- GridTemplate.builder().setTitle("Title").setSingleList(itemList).build();
+ GridTemplate.builder().setTitle("Title 1").setSingleList(itemList).build();
assertThat(template)
.isNotEqualTo(
@@ -170,7 +170,8 @@
.setTitle("Title")
.setSingleList(
ItemList.builder().addItem(
- GridItem.builder().setImage(BACK).build()).build())
+ GridItem.builder().setTitle("Title 2").setImage(
+ BACK).build()).build())
.build());
}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ItemListTest.java b/car/app/app/src/test/java/androidx/car/app/model/ItemListTest.java
similarity index 96%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/ItemListTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/ItemListTest.java
index 90df854..e306d1b 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/ItemListTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/ItemListTest.java
@@ -35,9 +35,6 @@
import androidx.car.app.WrappedRuntimeException;
import androidx.car.app.model.ItemList.OnItemVisibilityChangedListener;
import androidx.car.app.model.ItemList.OnSelectedListener;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Rule;
@@ -48,12 +45,14 @@
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
import java.util.Collections;
/** Tests for {@link ItemListTest}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class ItemListTest {
@Rule
public final MockitoRule mockito = MockitoJUnit.rule();
@@ -85,8 +84,8 @@
@Test
public void createGridItems() {
- GridItem gridItem1 = GridItem.builder().setImage(BACK).build();
- GridItem gridItem2 = GridItem.builder().setImage(BACK).build();
+ GridItem gridItem1 = GridItem.builder().setTitle("title 1").setImage(BACK).build();
+ GridItem gridItem2 = GridItem.builder().setTitle("title 2").setImage(BACK).build();
ItemList list = builder().addItem(gridItem1).addItem(gridItem2).build();
assertThat(list.getItems()).containsExactly(gridItem1, gridItem2).inOrder();
@@ -114,7 +113,6 @@
}
@Test
- @UiThreadTest
public void setSelectable() throws RemoteException {
OnSelectedListener mockListener = mock(OnSelectedListener.class);
ItemList itemList =
@@ -163,7 +161,6 @@
}
@Test
- @UiThreadTest
public void setOnItemVisibilityChangeListener_triggerListener() {
OnItemVisibilityChangedListener listener = mock(OnItemVisibilityChangedListener.class);
ItemList list =
@@ -185,7 +182,6 @@
}
@Test
- @UiThreadTest
public void setOnItemVisibilityChangeListener_triggerListenerWithFailure() {
OnItemVisibilityChangedListener listener = mock(OnItemVisibilityChangedListener.class);
ItemList list =
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/LatLngTest.java b/car/app/app/src/test/java/androidx/car/app/model/LatLngTest.java
similarity index 91%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/LatLngTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/LatLngTest.java
index c170d65..86b24de 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/LatLngTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/LatLngTest.java
@@ -18,15 +18,14 @@
import static com.google.common.truth.Truth.assertThat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link LatLng}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class LatLngTest {
@Test
public void createInstance() {
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ListTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/model/ListTemplateTest.java
similarity index 98%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/ListTemplateTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/ListTemplateTest.java
index 7bf4812..30d52b0 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/ListTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/ListTemplateTest.java
@@ -20,15 +20,14 @@
import static org.junit.Assert.assertThrows;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link ListTemplate}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class ListTemplateTest {
@Test
public void createInstance_emptyList_notLoading_Throws() {
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/MessageTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/model/MessageTemplateTest.java
similarity index 97%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/MessageTemplateTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/MessageTemplateTest.java
index 2d6a0d4..eb791af 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/MessageTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/MessageTemplateTest.java
@@ -27,17 +27,17 @@
import android.util.Log;
import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link MessageTemplate}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class MessageTemplateTest {
private final String mTitle = "header";
@@ -82,7 +82,7 @@
assertThat(template.getTitle().getText()).isEqualTo("header");
assertThat(template.getIcon()).isNull();
assertThat(template.getHeaderAction()).isNull();
- assertThat(template.getActionList()).isNull();
+ assertThat(template.getActions()).isNull();
assertThat(template.getDebugMessage()).isNull();
}
@@ -121,7 +121,7 @@
Log.getStackTraceString(exception));
assertThat(template.getIcon()).isEqualTo(icon);
assertThat(template.getHeaderAction()).isEqualTo(Action.BACK);
- assertThat(template.getActionList().getList()).containsExactly(action);
+ assertThat(template.getActions().getList()).containsExactly(action);
}
@Test
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/MetadataTest.java b/car/app/app/src/test/java/androidx/car/app/model/MetadataTest.java
similarity index 92%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/MetadataTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/MetadataTest.java
index a67ff2d..e3310f11 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/MetadataTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/MetadataTest.java
@@ -18,15 +18,14 @@
import static com.google.common.truth.Truth.assertThat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for the {@link Metadata} class. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class MetadataTest {
@Test
public void setAndGetPlace() {
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ModelUtilsTest.java b/car/app/app/src/test/java/androidx/car/app/model/ModelUtilsTest.java
similarity index 79%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/ModelUtilsTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/ModelUtilsTest.java
index 4a82bff..bbf190f 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/ModelUtilsTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/ModelUtilsTest.java
@@ -20,26 +20,24 @@
import android.text.SpannableString;
-import androidx.car.app.test.R;
-import androidx.core.graphics.drawable.IconCompat;
+import androidx.car.app.TestUtils;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link PlaceListMapTemplate}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class ModelUtilsTest {
@Test
public void validateAllNonBrowsableRowsHaveDistances() {
- DistanceSpan span =
- DistanceSpan.create(
- Distance.create(/* displayDistance= */ 1, Distance.UNIT_KILOMETERS_P1));
+ DistanceSpan span = DistanceSpan.create(
+ Distance.create(/* displayDistance= */ 1, Distance.UNIT_KILOMETERS_P1));
SpannableString stringWithDistance = new SpannableString("Test");
stringWithDistance.setSpan(span, /* start= */ 0, /* end= */ 1, /* flags= */ 0);
SpannableString stringWithInvalidDistance = new SpannableString("Test");
@@ -56,14 +54,12 @@
assertThrows(
IllegalArgumentException.class,
- () ->
- ModelUtils.validateAllNonBrowsableRowsHaveDistance(
- ImmutableList.of(rowWithDistance, rowWithInvalidDistance)));
+ () -> ModelUtils.validateAllNonBrowsableRowsHaveDistance(
+ ImmutableList.of(rowWithDistance, rowWithInvalidDistance)));
assertThrows(
IllegalArgumentException.class,
- () ->
- ModelUtils.validateAllNonBrowsableRowsHaveDistance(
- ImmutableList.of(rowWithDistance, rowWithoutDistance)));
+ () -> ModelUtils.validateAllNonBrowsableRowsHaveDistance(
+ ImmutableList.of(rowWithDistance, rowWithoutDistance)));
// Positive cases
ModelUtils.validateAllNonBrowsableRowsHaveDistance(ImmutableList.of());
@@ -102,14 +98,12 @@
assertThrows(
IllegalArgumentException.class,
- () ->
- ModelUtils.validateAllRowsHaveDistanceOrDuration(
- ImmutableList.of(rowWithDuration, rowWithInvalidDuration)));
+ () -> ModelUtils.validateAllRowsHaveDistanceOrDuration(
+ ImmutableList.of(rowWithDuration, rowWithInvalidDuration)));
assertThrows(
IllegalArgumentException.class,
- () ->
- ModelUtils.validateAllRowsHaveDistanceOrDuration(
- ImmutableList.of(rowWithDuration, plainRow)));
+ () -> ModelUtils.validateAllRowsHaveDistanceOrDuration(
+ ImmutableList.of(rowWithDuration, plainRow)));
// Positive cases.
ModelUtils.validateAllRowsHaveDistanceOrDuration(ImmutableList.of());
@@ -123,10 +117,8 @@
@Test
public void validateAllRowsHaveOnlySmallSizedImages() {
- CarIcon carIcon =
- CarIcon.of(
- IconCompat.createWithResource(
- ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+ CarIcon carIcon = TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+ "ic_test_1");
Row rowWithNoImage = Row.builder().setTitle("title1").build();
Row rowWithSmallImage =
Row.builder().setTitle("title2").setImage(carIcon, Row.IMAGE_TYPE_SMALL).build();
@@ -139,9 +131,8 @@
ImmutableList.of(rowWithLargeImage)));
assertThrows(
IllegalArgumentException.class,
- () ->
- ModelUtils.validateAllRowsHaveOnlySmallImages(
- ImmutableList.of(rowWithNoImage, rowWithLargeImage)));
+ () -> ModelUtils.validateAllRowsHaveOnlySmallImages(
+ ImmutableList.of(rowWithNoImage, rowWithLargeImage)));
// Positive cases
ModelUtils.validateAllRowsHaveOnlySmallImages(ImmutableList.of());
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/OnClickListenerWrapperTest.java b/car/app/app/src/test/java/androidx/car/app/model/OnClickListenerWrapperTest.java
similarity index 88%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/OnClickListenerWrapperTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/OnClickListenerWrapperTest.java
index 5db449f..3afb7ee 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/OnClickListenerWrapperTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/OnClickListenerWrapperTest.java
@@ -24,9 +24,6 @@
import static org.mockito.Mockito.verify;
import androidx.car.app.OnDoneCallback;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Rule;
import org.junit.Test;
@@ -34,9 +31,11 @@
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class OnClickListenerWrapperTest {
@Rule
public final MockitoRule mockito = MockitoJUnit.rule();
@@ -45,7 +44,6 @@
OnClickListener mMockOnClickListener;
@Test
- @UiThreadTest
public void create() {
OnClickListenerWrapper wrapper = OnClickListenerWrapperImpl.create(mMockOnClickListener);
assertThat(wrapper.isParkedOnly()).isFalse();
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/PaneTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/model/PaneTemplateTest.java
similarity index 97%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/PaneTemplateTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/PaneTemplateTest.java
index f2ffb88..80f0bf3 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/PaneTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/PaneTemplateTest.java
@@ -21,15 +21,15 @@
import static org.junit.Assert.assertThrows;
import androidx.car.app.TestUtils;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link PaneTemplate}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class PaneTemplateTest {
@Test
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/PaneTest.java b/car/app/app/src/test/java/androidx/car/app/model/PaneTest.java
similarity index 95%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/PaneTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/PaneTest.java
index a7bd7aa..9fdb2f3 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/PaneTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/PaneTest.java
@@ -20,20 +20,19 @@
import static org.junit.Assert.assertThrows;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
import java.util.Arrays;
import java.util.List;
/** Tests for {@link Pane}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class PaneTest {
@Test
public void createEmptyRows_throws() {
@@ -78,7 +77,7 @@
Pane pane =
Pane.builder().addRow(Row.builder().setTitle("Title").build()).setActions(
actions).build();
- assertActions(pane.getActionList(), actions);
+ assertActions(pane.getActions(), actions);
}
@Test
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ParkedOnlyOnClickListenerTest.java b/car/app/app/src/test/java/androidx/car/app/model/ParkedOnlyOnClickListenerTest.java
similarity index 90%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/ParkedOnlyOnClickListenerTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/ParkedOnlyOnClickListenerTest.java
index 8b9d03a..d871996 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/ParkedOnlyOnClickListenerTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/ParkedOnlyOnClickListenerTest.java
@@ -24,9 +24,6 @@
import android.os.RemoteException;
import androidx.car.app.OnDoneCallback;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Rule;
import org.junit.Test;
@@ -34,10 +31,12 @@
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link OnClickListenerWrapper}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class ParkedOnlyOnClickListenerTest {
@Rule
public final MockitoRule mockito = MockitoJUnit.rule();
@@ -46,7 +45,6 @@
OnClickListener mMockOnClickListener;
@Test
- @UiThreadTest
public void create() throws RemoteException {
ParkedOnlyOnClickListener parkedOnlyOnClickListener =
ParkedOnlyOnClickListener.create(mMockOnClickListener);
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceListMapTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/model/PlaceListMapTemplateTest.java
similarity index 98%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/PlaceListMapTemplateTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/PlaceListMapTemplateTest.java
index 20ce3a7..d590b0b 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceListMapTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/PlaceListMapTemplateTest.java
@@ -25,15 +25,15 @@
import androidx.car.app.TestUtils;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link PlaceListMapTemplate}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class PlaceListMapTemplateTest {
private final Context mContext = ApplicationProvider.getApplicationContext();
private final DistanceSpan mDistanceSpan =
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceMarkerTest.java b/car/app/app/src/test/java/androidx/car/app/model/PlaceMarkerTest.java
similarity index 67%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/PlaceMarkerTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/PlaceMarkerTest.java
index 3e7cc36..41a25e1 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceMarkerTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/PlaceMarkerTest.java
@@ -26,18 +26,18 @@
import android.content.ContentResolver;
import android.net.Uri;
-import androidx.car.app.test.R;
+import androidx.car.app.TestUtils;
import androidx.core.graphics.drawable.IconCompat;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link PlaceMarker}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class PlaceMarkerTest {
@Test
@@ -48,17 +48,14 @@
@Test
public void setColor_withImageTypeIcon_throws() {
- CarIcon icon =
- CarIcon.of(
- IconCompat.createWithResource(
- ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+ CarIcon icon = TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+ "ic_test_1");
assertThrows(
IllegalStateException.class,
- () ->
- PlaceMarker.builder()
- .setIcon(icon, PlaceMarker.TYPE_IMAGE)
- .setColor(CarColor.SECONDARY)
- .build());
+ () -> PlaceMarker.builder()
+ .setIcon(icon, PlaceMarker.TYPE_IMAGE)
+ .setColor(CarColor.SECONDARY)
+ .build());
}
@Test
@@ -75,16 +72,13 @@
@Test
public void createInstance() {
- CarIcon icon =
- CarIcon.of(
- IconCompat.createWithResource(
- ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
- PlaceMarker marker1 =
- PlaceMarker.builder()
- .setIcon(icon, PlaceMarker.TYPE_ICON)
- .setLabel("foo")
- .setColor(CarColor.SECONDARY)
- .build();
+ CarIcon icon = TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+ "ic_test_1");
+ PlaceMarker marker1 = PlaceMarker.builder()
+ .setIcon(icon, PlaceMarker.TYPE_ICON)
+ .setLabel("foo")
+ .setColor(CarColor.SECONDARY)
+ .build();
assertThat(marker1.getIcon()).isEqualTo(icon);
assertThat(marker1.getIconType()).isEqualTo(PlaceMarker.TYPE_ICON);
assertThat(marker1.getColor()).isEqualTo(CarColor.SECONDARY);
@@ -103,23 +97,19 @@
@Test
public void equals() {
- CarIcon carIcon =
- CarIcon.of(
- IconCompat.createWithResource(
- ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
- PlaceMarker marker =
- PlaceMarker.builder()
- .setIcon(carIcon, PlaceMarker.TYPE_ICON)
- .setLabel("foo")
- .setColor(CarColor.SECONDARY)
- .build();
+ CarIcon carIcon = TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+ "ic_test_1");
+ PlaceMarker marker = PlaceMarker.builder()
+ .setIcon(carIcon, PlaceMarker.TYPE_ICON)
+ .setLabel("foo")
+ .setColor(CarColor.SECONDARY)
+ .build();
- assertThat(
- PlaceMarker.builder()
- .setIcon(carIcon, PlaceMarker.TYPE_ICON)
- .setLabel("foo")
- .setColor(CarColor.SECONDARY)
- .build())
+ assertThat(PlaceMarker.builder()
+ .setIcon(carIcon, PlaceMarker.TYPE_ICON)
+ .setLabel("foo")
+ .setColor(CarColor.SECONDARY)
+ .build())
.isEqualTo(marker);
}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceTest.java b/car/app/app/src/test/java/androidx/car/app/model/PlaceTest.java
similarity index 93%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/PlaceTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/PlaceTest.java
index 664dd26..d4ef129 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/PlaceTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/PlaceTest.java
@@ -18,15 +18,14 @@
import static com.google.common.truth.Truth.assertThat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for the {@link Place} class. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class PlaceTest {
/** Tests basic setter and getter operations. */
@Test
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/RowTest.java b/car/app/app/src/test/java/androidx/car/app/model/RowTest.java
similarity index 93%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/RowTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/RowTest.java
index 73a62dd..69c140b 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/RowTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/RowTest.java
@@ -26,19 +26,17 @@
import static org.mockito.Mockito.verify;
import androidx.car.app.OnDoneCallback;
-import androidx.car.app.test.R;
-import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.annotation.UiThreadTest;
+import androidx.car.app.TestUtils;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link Row}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class RowTest {
@Test
public void create_defaultValues() {
@@ -98,7 +96,6 @@
}
@Test
- @UiThreadTest
public void clickListener() {
OnClickListener onClickListener = mock(OnClickListener.class);
Row row = Row.builder().setTitle("Title").setOnClickListener(onClickListener).build();
@@ -145,11 +142,8 @@
})
.setTitle("Title")
.addText("Text")
- .setImage(
- CarIcon.of(
- IconCompat.createWithResource(
- ApplicationProvider.getApplicationContext(),
- R.drawable.ic_test_1)))
+ .setImage(TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+ "ic_test_1"))
.build();
}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/SearchTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/model/SearchTemplateTest.java
similarity index 97%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/SearchTemplateTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/SearchTemplateTest.java
index 9a754f2..9126708 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/SearchTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/SearchTemplateTest.java
@@ -27,12 +27,8 @@
import android.os.RemoteException;
import androidx.car.app.OnDoneCallback;
-import androidx.car.app.SearchListener;
import androidx.car.app.TestUtils;
import androidx.car.app.WrappedRuntimeException;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Rule;
import org.junit.Test;
@@ -40,16 +36,18 @@
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link SearchTemplate}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class SearchTemplateTest {
@Rule
public final MockitoRule mockito = MockitoJUnit.rule();
@Mock
- SearchListener mMockSearchListener;
+ SearchTemplate.SearchListener mMockSearchListener;
@Test
public void createInstance_isLoading_hasList_Throws() {
@@ -124,7 +122,6 @@
}
@Test
- @UiThreadTest
public void buildWithValues() throws RemoteException {
String initialSearchText = "searchTemplate for this!!";
String searchHint = "This is not a hint";
@@ -155,7 +152,6 @@
}
@Test
- @UiThreadTest
public void buildWithValues_failureOnSearchSubmitted() throws RemoteException {
String initialSearchText = "searchTemplate for this!!";
String searchHint = "This is not a hint";
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/SectionedItemListTest.java b/car/app/app/src/test/java/androidx/car/app/model/SectionedItemListTest.java
similarity index 94%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/SectionedItemListTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/SectionedItemListTest.java
index 69f5124..b80e38a 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/SectionedItemListTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/SectionedItemListTest.java
@@ -18,15 +18,14 @@
import static com.google.common.truth.Truth.assertThat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link ItemListTest}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class SectionedItemListTest {
@Test
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/TemplateWrapperTest.java b/car/app/app/src/test/java/androidx/car/app/model/TemplateWrapperTest.java
similarity index 95%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/TemplateWrapperTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/TemplateWrapperTest.java
index 3cbb446..f8ff387 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/TemplateWrapperTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/TemplateWrapperTest.java
@@ -18,15 +18,14 @@
import static com.google.common.truth.Truth.assertThat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link TemplateWrapper}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class TemplateWrapperTest {
@Test
public void createInstance() {
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ToggleTest.java b/car/app/app/src/test/java/androidx/car/app/model/ToggleTest.java
similarity index 93%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/ToggleTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/ToggleTest.java
index dad2cbd..d4d3575 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/ToggleTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/ToggleTest.java
@@ -26,9 +26,6 @@
import androidx.car.app.OnDoneCallback;
import androidx.car.app.WrappedRuntimeException;
import androidx.car.app.model.Toggle.OnCheckedChangeListener;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Rule;
import org.junit.Test;
@@ -36,10 +33,12 @@
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link Toggle}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class ToggleTest {
@Rule
public MockitoRule mocks = MockitoJUnit.rule();
@@ -54,7 +53,6 @@
}
@Test
- @UiThreadTest
public void build_checkedChange_sendsCheckedChangeCall() {
Toggle toggle = Toggle.builder(mMockOnCheckedChangeListener).setChecked(true).build();
OnDoneCallback onDoneCallback = mock(OnDoneCallback.class);
@@ -65,7 +63,6 @@
}
@Test
- @UiThreadTest
public void build_checkedChange_sendsCheckedChangeCallWithFailure() {
String testExceptionMessage = "Test exception";
doThrow(new RuntimeException(testExceptionMessage)).when(
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/ActionsConstraintsTest.java b/car/app/app/src/test/java/androidx/car/app/model/constraints/ActionsConstraintsTest.java
similarity index 63%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/constraints/ActionsConstraintsTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/constraints/ActionsConstraintsTest.java
index ab577b5..0ac564e 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/ActionsConstraintsTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/constraints/ActionsConstraintsTest.java
@@ -24,20 +24,18 @@
import androidx.car.app.model.Action;
import androidx.car.app.model.ActionStrip;
import androidx.car.app.model.CarIcon;
-import androidx.car.app.test.R;
-import androidx.core.graphics.drawable.IconCompat;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
import java.util.Collections;
/** Tests for {@link ActionsConstraints}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class ActionsConstraintsTest {
@Test
public void createEmpty() {
@@ -51,23 +49,21 @@
public void create_requiredExceedsMaxAllowedActions() {
assertThrows(
IllegalArgumentException.class,
- () ->
- ActionsConstraints.builder()
- .setMaxActions(1)
- .addRequiredActionType(Action.TYPE_BACK)
- .addRequiredActionType(Action.TYPE_CUSTOM)
- .build());
+ () -> ActionsConstraints.builder()
+ .setMaxActions(1)
+ .addRequiredActionType(Action.TYPE_BACK)
+ .addRequiredActionType(Action.TYPE_CUSTOM)
+ .build());
}
@Test
public void create_requiredAlsoDisallowed() {
assertThrows(
IllegalArgumentException.class,
- () ->
- ActionsConstraints.builder()
- .addRequiredActionType(Action.TYPE_BACK)
- .addDisallowedActionType(Action.TYPE_BACK)
- .build());
+ () -> ActionsConstraints.builder()
+ .addRequiredActionType(Action.TYPE_BACK)
+ .addDisallowedActionType(Action.TYPE_BACK)
+ .build());
}
@Test
@@ -94,10 +90,8 @@
.addDisallowedActionType(Action.TYPE_BACK)
.build();
- CarIcon carIcon =
- CarIcon.of(
- IconCompat.createWithResource(
- ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+ CarIcon carIcon = TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+ "ic_test_1");
Action actionWithIcon = TestUtils.createAction(null, carIcon);
Action actionWithTitle = TestUtils.createAction("Title", carIcon);
@@ -115,39 +109,35 @@
// Missing required type.
assertThrows(
IllegalArgumentException.class,
- () ->
- constraints.validateOrThrow(
- ActionStrip.builder().addAction(
- Action.APP_ICON).build().getActions()));
+ () -> constraints.validateOrThrow(
+ ActionStrip.builder().addAction(
+ Action.APP_ICON).build().getActions()));
// Disallowed type
assertThrows(
IllegalArgumentException.class,
- () ->
- constraints.validateOrThrow(
- ActionStrip.builder().addAction(Action.BACK).build().getActions()));
+ () -> constraints.validateOrThrow(
+ ActionStrip.builder().addAction(Action.BACK).build().getActions()));
// Over max allowed actions
assertThrows(
IllegalArgumentException.class,
- () ->
- constraints.validateOrThrow(
- ActionStrip.builder()
- .addAction(Action.APP_ICON)
- .addAction(actionWithIcon)
- .addAction(actionWithTitle)
- .build()
- .getActions()));
+ () -> constraints.validateOrThrow(
+ ActionStrip.builder()
+ .addAction(Action.APP_ICON)
+ .addAction(actionWithIcon)
+ .addAction(actionWithTitle)
+ .build()
+ .getActions()));
// Over max allowed actions with title
assertThrows(
IllegalArgumentException.class,
- () ->
- constraints.validateOrThrow(
- ActionStrip.builder()
- .addAction(actionWithTitle)
- .addAction(actionWithTitle)
- .build()
- .getActions()));
+ () -> constraints.validateOrThrow(
+ ActionStrip.builder()
+ .addAction(actionWithTitle)
+ .addAction(actionWithTitle)
+ .build()
+ .getActions()));
}
}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowConstraintsTest.java b/car/app/app/src/test/java/androidx/car/app/model/constraints/RowConstraintsTest.java
similarity index 66%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowConstraintsTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/constraints/RowConstraintsTest.java
index 9b4d96e..5bc88c5 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowConstraintsTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/constraints/RowConstraintsTest.java
@@ -18,21 +18,20 @@
import static org.junit.Assert.assertThrows;
+import androidx.car.app.TestUtils;
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.Row;
import androidx.car.app.model.Toggle;
-import androidx.car.app.test.R;
-import androidx.core.graphics.drawable.IconCompat;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link RowConstraints}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class RowConstraintsTest {
@Test
public void validate_clickListener() {
@@ -43,10 +42,9 @@
assertThrows(
IllegalArgumentException.class,
- () ->
- constraints.validateOrThrow(
- Row.builder().setTitle("Title)").setOnClickListener(() -> {
- }).build()));
+ () -> constraints.validateOrThrow(
+ Row.builder().setTitle("Title)").setOnClickListener(() -> {
+ }).build()));
// Positive cases
constraints.validateOrThrow(Row.builder().setTitle("Title").build());
@@ -62,13 +60,12 @@
assertThrows(
IllegalArgumentException.class,
- () ->
- constraints.validateOrThrow(
- Row.builder()
- .setTitle("Title)")
- .setToggle(Toggle.builder(isChecked -> {
- }).build())
- .build()));
+ () -> constraints.validateOrThrow(
+ Row.builder()
+ .setTitle("Title)")
+ .setToggle(Toggle.builder(isChecked -> {
+ }).build())
+ .build()));
// Positive cases
constraints.validateOrThrow(Row.builder().setTitle("Title").build());
@@ -81,16 +78,13 @@
public void validate_images() {
RowConstraints constraints = RowConstraints.builder().setImageAllowed(false).build();
RowConstraints allowConstraints = RowConstraints.builder().setImageAllowed(true).build();
- CarIcon carIcon =
- CarIcon.of(
- IconCompat.createWithResource(
- ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+ CarIcon carIcon = TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+ "ic_test_1");
assertThrows(
IllegalArgumentException.class,
- () ->
- constraints.validateOrThrow(
- Row.builder().setTitle("Title)").setImage(carIcon).build()));
+ () -> constraints.validateOrThrow(
+ Row.builder().setTitle("Title)").setImage(carIcon).build()));
// Positive cases
constraints.validateOrThrow(Row.builder().setTitle("Title").build());
@@ -103,14 +97,13 @@
assertThrows(
IllegalArgumentException.class,
- () ->
- constraints.validateOrThrow(
- Row.builder()
- .setTitle("Title)")
- .addText("text1")
- .addText("text2")
- .addText("text3")
- .build()));
+ () -> constraints.validateOrThrow(
+ Row.builder()
+ .setTitle("Title)")
+ .addText("text1")
+ .addText("text2")
+ .addText("text3")
+ .build()));
// Positive cases
constraints.validateOrThrow(
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowListConstraintsTest.java b/car/app/app/src/test/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
similarity index 95%
rename from car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
rename to car/app/app/src/test/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
index 56b5273..131dd52 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
@@ -19,15 +19,15 @@
import static org.junit.Assert.assertThrows;
import androidx.car.app.TestUtils;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link RowListConstraints}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class RowListConstraintsTest {
@Test
public void validate_itemList_noSelectable() {
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/NavigationManagerTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/NavigationManagerTest.java
similarity index 78%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/NavigationManagerTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/NavigationManagerTest.java
index 051358e..6605c9b 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/NavigationManagerTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/NavigationManagerTest.java
@@ -37,9 +37,8 @@
import androidx.car.app.navigation.model.TravelEstimate;
import androidx.car.app.navigation.model.Trip;
import androidx.car.app.serialization.Bundleable;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
+import androidx.car.app.testing.TestCarContext;
+import androidx.test.core.app.ApplicationProvider;
import org.junit.Before;
import org.junit.Test;
@@ -47,12 +46,15 @@
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
/** Tests for {@link NavigationManager}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class NavigationManagerTest {
@Mock
private ICarHost mMockCarHost;
@@ -88,10 +90,12 @@
.build();
@Before
- @UiThreadTest
public void setUp() throws RemoteException {
MockitoAnnotations.initMocks(this);
+ TestCarContext testCarContext =
+ TestCarContext.createCarContext(ApplicationProvider.getApplicationContext());
+
INavigationHost navHostStub =
new INavigationHost.Stub() {
@Override
@@ -113,15 +117,14 @@
mHostDispatcher.setCarHost(mMockCarHost);
- mNavigationManager = NavigationManager.create(mHostDispatcher);
+ mNavigationManager = NavigationManager.create(testCarContext, mHostDispatcher);
}
@Test
- @UiThreadTest
public void navigationStarted_sendState_navigationEnded() throws RemoteException {
InOrder inOrder = inOrder(mMockNavHost);
- mNavigationManager.setListener(mNavigationListener);
+ mNavigationManager.setNavigationManagerListener(mNavigationListener);
mNavigationManager.navigationStarted();
inOrder.verify(mMockNavHost).navigationStarted();
@@ -133,16 +136,14 @@
}
@Test
- @UiThreadTest
public void navigationStarted_noListenerSet() throws RemoteException {
assertThrows(IllegalStateException.class, () -> mNavigationManager.navigationStarted());
}
@Test
- @UiThreadTest
public void navigationStarted_multiple() throws RemoteException {
- mNavigationManager.setListener(mNavigationListener);
+ mNavigationManager.setNavigationManagerListener(mNavigationListener);
mNavigationManager.navigationStarted();
mNavigationManager.navigationStarted();
@@ -150,7 +151,6 @@
}
@Test
- @UiThreadTest
public void navigationEnded_multiple_not_started() throws RemoteException {
mNavigationManager.navigationEnded();
mNavigationManager.navigationEnded();
@@ -164,44 +164,52 @@
}
@Test
- @UiThreadTest
- public void stopNavigation_notNavigating() throws RemoteException {
- mNavigationManager.setListener(mNavigationListener);
- mNavigationManager.getIInterface().stopNavigation(mock(IOnDoneCallback.class));
- verify(mNavigationListener, never()).stopNavigation();
+ public void onStopNavigation_notNavigating() throws RemoteException {
+ mNavigationManager.setNavigationManagerListener(mNavigationListener);
+ mNavigationManager.getIInterface().onStopNavigation(mock(IOnDoneCallback.class));
+ verify(mNavigationListener, never()).onStopNavigation();
}
@Test
- @UiThreadTest
- public void stopNavigation_navigating_restart() throws RemoteException {
+ public void onStopNavigation_navigating_restart() throws RemoteException {
InOrder inOrder = inOrder(mMockNavHost, mNavigationListener);
- mNavigationManager.setListener(mNavigationListener);
+ mNavigationManager.setNavigationManagerListener(new SynchronousExecutor(),
+ mNavigationListener);
mNavigationManager.navigationStarted();
inOrder.verify(mMockNavHost).navigationStarted();
- mNavigationManager.getIInterface().stopNavigation(mock(IOnDoneCallback.class));
- inOrder.verify(mNavigationListener).stopNavigation();
+ mNavigationManager.getIInterface().onStopNavigation(mock(IOnDoneCallback.class));
+
+ inOrder.verify(mNavigationListener).onStopNavigation();
mNavigationManager.navigationStarted();
inOrder.verify(mMockNavHost).navigationStarted();
}
@Test
- @UiThreadTest
public void onAutoDriveEnabled_callsListener() {
- mNavigationManager.setListener(mNavigationListener);
+ mNavigationManager.setNavigationManagerListener(new SynchronousExecutor(),
+ mNavigationListener);
mNavigationManager.onAutoDriveEnabled();
verify(mNavigationListener).onAutoDriveEnabled();
}
@Test
- @UiThreadTest
public void onAutoDriveEnabledBeforeRegisteringListener_callsListener() {
mNavigationManager.onAutoDriveEnabled();
- mNavigationManager.setListener(mNavigationListener);
+ mNavigationManager.setNavigationManagerListener(new SynchronousExecutor(),
+ mNavigationListener);
verify(mNavigationListener).onAutoDriveEnabled();
}
+
+ static class SynchronousExecutor implements Executor {
+ @Override
+ public void execute(Runnable r) {
+ r.run();
+ }
+ }
+
}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/DestinationTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/DestinationTest.java
similarity index 96%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/DestinationTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/DestinationTest.java
index ba52c24..0663a13 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/DestinationTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/DestinationTest.java
@@ -25,15 +25,15 @@
import androidx.car.app.model.CarIcon;
import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link Destination}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class DestinationTest {
@Test
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneDirectionTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/LaneDirectionTest.java
similarity index 92%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneDirectionTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/LaneDirectionTest.java
index 3f17b37..3d691ec 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneDirectionTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/LaneDirectionTest.java
@@ -20,15 +20,14 @@
import static com.google.common.truth.Truth.assertThat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link LaneDirection}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class LaneDirectionTest {
@Test
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/LaneTest.java
similarity index 94%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/LaneTest.java
index 8d4a1197..aee2408 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/LaneTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/LaneTest.java
@@ -21,15 +21,14 @@
import static com.google.common.truth.Truth.assertThat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link Lane}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class LaneTest {
@Test
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/ManeuverTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/ManeuverTest.java
similarity index 98%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/ManeuverTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/ManeuverTest.java
index c5d2636..5064bda 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/ManeuverTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/ManeuverTest.java
@@ -33,15 +33,15 @@
import androidx.car.app.model.CarIcon;
import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link Maneuver}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class ManeuverTest {
@Test
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/MessageInfoTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/MessageInfoTest.java
similarity index 95%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/MessageInfoTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/MessageInfoTest.java
index 1be5851..81063ca 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/MessageInfoTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/MessageInfoTest.java
@@ -25,15 +25,15 @@
import androidx.car.app.model.CarIcon;
import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link MessageInfoTest}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class MessageInfoTest {
@Test
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/NavigationTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/NavigationTemplateTest.java
similarity index 98%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/NavigationTemplateTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/NavigationTemplateTest.java
index 2cbd5c4..8e3b04a 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/NavigationTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/NavigationTemplateTest.java
@@ -29,17 +29,17 @@
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.Distance;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
import java.util.concurrent.TimeUnit;
/** Tests for {@link NavigationTemplate}. */
-@LargeTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class NavigationTemplateTest {
private final ActionStrip mActionStrip =
ActionStrip.builder().addAction(TestUtils.createAction("test", null)).build();
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java
similarity index 98%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java
index 844c094..cd5aa98 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/PlaceListNavigationTemplateTest.java
@@ -38,15 +38,15 @@
import androidx.car.app.model.Row;
import androidx.car.app.model.Toggle;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link PlaceListNavigationTemplate}. */
-@LargeTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class PlaceListNavigationTemplateTest {
private final Context mContext = ApplicationProvider.getApplicationContext();
private final DistanceSpan mDistanceSpan =
@@ -60,7 +60,7 @@
() -> PlaceListNavigationTemplate.builder().setTitle("Title").build());
// Positive case
- PlaceListNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+ PlaceListNavigationTemplate.builder().setTitle("Title").setLoading(true).build();
}
@Test
@@ -70,7 +70,7 @@
() ->
PlaceListNavigationTemplate.builder()
.setTitle("Title")
- .setIsLoading(true)
+ .setLoading(true)
.setItemList(ItemList.builder().build())
.build());
}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java
similarity index 95%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java
index 57a4723..dcf4c11 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/RoutePreviewNavigationTemplateTest.java
@@ -36,19 +36,16 @@
import androidx.car.app.model.ItemList;
import androidx.car.app.model.OnClickListener;
import androidx.car.app.model.Row;
-import androidx.car.app.test.R;
-import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.annotation.UiThreadTest;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link RoutePreviewNavigationTemplate}. */
-@LargeTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class RoutePreviewNavigationTemplateTest {
private final Context mContext = ApplicationProvider.getApplicationContext();
private static final DistanceSpan DISTANCE =
@@ -62,7 +59,7 @@
() -> RoutePreviewNavigationTemplate.builder().setTitle("Title").build());
// Positive case
- RoutePreviewNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+ RoutePreviewNavigationTemplate.builder().setTitle("Title").setLoading(true).build();
}
@Test
@@ -71,7 +68,7 @@
IllegalStateException.class,
() -> RoutePreviewNavigationTemplate.builder()
.setTitle("Title")
- .setIsLoading(true)
+ .setLoading(true)
.setItemList(
TestUtils.createItemListWithDistanceSpan(2, true, DISTANCE))
.build());
@@ -127,13 +124,13 @@
public void noHeaderTitleOrAction_throws() {
assertThrows(
IllegalStateException.class,
- () -> RoutePreviewNavigationTemplate.builder().setIsLoading(true).build());
+ () -> RoutePreviewNavigationTemplate.builder().setLoading(true).build());
// Positive cases.
- RoutePreviewNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+ RoutePreviewNavigationTemplate.builder().setTitle("Title").setLoading(true).build();
RoutePreviewNavigationTemplate.builder()
.setHeaderAction(Action.BACK)
- .setIsLoading(true)
+ .setLoading(true)
.build();
}
@@ -185,7 +182,6 @@
}
@Test
- @UiThreadTest
public void setOnNavigateAction() {
OnClickListener mockListener = mock(OnClickListener.class);
RoutePreviewNavigationTemplate template =
@@ -216,7 +212,7 @@
.build());
// Positive case
- RoutePreviewNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+ RoutePreviewNavigationTemplate.builder().setTitle("Title").setLoading(true).build();
}
@Test
@@ -234,13 +230,13 @@
.build());
// Positive case
- RoutePreviewNavigationTemplate.builder().setTitle("Title").setIsLoading(true).build();
+ RoutePreviewNavigationTemplate.builder().setTitle("Title").setLoading(true).build();
}
@Test
public void createInstance_navigateActionNoTitle_throws() {
- CarIcon carIcon = CarIcon.of(IconCompat.createWithResource(
- ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+ CarIcon carIcon = TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+ "ic_test_1");
assertThrows(
IllegalArgumentException.class,
() -> RoutePreviewNavigationTemplate.builder()
@@ -271,9 +267,8 @@
Row rowWithTime = Row.builder().setTitle(title).build();
Row rowWithoutTime = Row.builder().setTitle("Google Bve").build();
Action navigateAction = Action.builder()
- .setIcon(CarIcon.of(IconCompat.createWithResource(
- ApplicationProvider.getApplicationContext(),
- R.drawable.ic_test_1)))
+ .setIcon(TestUtils.getTestCarIcon(ApplicationProvider.getApplicationContext(),
+ "ic_test_1"))
.setTitle("Navigate")
.setOnClickListener(() -> {
})
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutingInfoTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/RoutingInfoTest.java
similarity index 97%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutingInfoTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/RoutingInfoTest.java
index 58ea716..8cd974c 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/RoutingInfoTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/RoutingInfoTest.java
@@ -26,15 +26,15 @@
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.Distance;
import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link RoutingInfoTest}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class RoutingInfoTest {
private final Maneuver mManeuver =
@@ -54,7 +54,7 @@
assertThrows(
IllegalStateException.class,
() -> RoutingInfo.builder()
- .setIsLoading(true)
+ .setLoading(true)
.setCurrentStep(mCurrentStep, mCurrentDistance)
.build());
}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/StepTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/StepTest.java
similarity index 97%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/StepTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/StepTest.java
index bc8a79d..3c0f0c8 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/StepTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/StepTest.java
@@ -24,15 +24,15 @@
import static org.junit.Assert.assertThrows;
import androidx.car.app.model.CarIcon;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests for {@link Step}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class StepTest {
@Test
public void createInstance() {
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TravelEstimateTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/TravelEstimateTest.java
similarity index 98%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TravelEstimateTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/TravelEstimateTest.java
index 7b51591..ca589ec 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TravelEstimateTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/TravelEstimateTest.java
@@ -27,12 +27,11 @@
import androidx.car.app.model.CarColor;
import androidx.car.app.model.DateTimeWithZone;
import androidx.car.app.model.Distance;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
import java.time.Duration;
import java.time.ZonedDateTime;
@@ -41,8 +40,8 @@
import java.util.concurrent.TimeUnit;
/** Tests for {@link TravelEstimate}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class TravelEstimateTest {
private final DateTimeWithZone mArrivalTime =
createDateTimeWithZone("2020-04-14T15:57:00", "US/Pacific");
@@ -66,7 +65,6 @@
}
@Test
- @SdkSuppress(minSdkVersion = 26)
public void create_duration() {
ZonedDateTime arrivalTime = ZonedDateTime.parse("2020-05-14T19:57:00-07:00[US/Pacific]");
Duration remainingTime = Duration.ofHours(10);
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TripTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/model/TripTest.java
similarity index 93%
rename from car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TripTest.java
rename to car/app/app/src/test/java/androidx/car/app/navigation/model/TripTest.java
index 4c936e3..7fb0d3c 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/navigation/model/TripTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/model/TripTest.java
@@ -26,17 +26,17 @@
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.Distance;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
import java.util.concurrent.TimeUnit;
/** Tests for {@link Trip}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class TripTest {
private final Step mStep =
@@ -71,7 +71,7 @@
.addDestinationTravelEstimate(mDestinationTravelEstimate)
.addStepTravelEstimate(mStepTravelEstimate)
.setCurrentRoad(ROAD)
- .setIsLoading(false)
+ .setLoading(false)
.build();
assertThat(trip.getDestinations()).hasSize(1);
@@ -112,7 +112,7 @@
.addDestination(mDestination)
.addDestinationTravelEstimate(mDestinationTravelEstimate)
.setCurrentRoad(ROAD)
- .setIsLoading(true)
+ .setLoading(true)
.build();
assertThat(trip.getDestinations()).hasSize(1);
@@ -129,17 +129,17 @@
public void createInstance_loading_with_steps() {
assertThrows(
IllegalArgumentException.class,
- () -> Trip.builder().addStep(mStep).setIsLoading(true).build());
+ () -> Trip.builder().addStep(mStep).setLoading(true).build());
assertThrows(
IllegalArgumentException.class,
- () -> Trip.builder().addStepTravelEstimate(mStepTravelEstimate).setIsLoading(
+ () -> Trip.builder().addStepTravelEstimate(mStepTravelEstimate).setLoading(
true).build());
assertThrows(
IllegalArgumentException.class,
() -> Trip.builder()
.addStep(mStep)
.addStepTravelEstimate(mStepTravelEstimate)
- .setIsLoading(true)
+ .setLoading(true)
.build());
}
}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/notification/CarAppExtenderTest.java b/car/app/app/src/test/java/androidx/car/app/notification/CarAppExtenderTest.java
similarity index 92%
rename from car/app/app/src/androidTest/java/androidx/car/app/notification/CarAppExtenderTest.java
rename to car/app/app/src/test/java/androidx/car/app/notification/CarAppExtenderTest.java
index 707e112..db35c5a 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/notification/CarAppExtenderTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/notification/CarAppExtenderTest.java
@@ -27,22 +27,21 @@
import android.os.Bundle;
import androidx.annotation.NonNull;
-import androidx.car.app.test.R;
+import androidx.car.app.TestUtils;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
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.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
import java.util.List;
/** Tests for {@link CarAppExtender}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public final class CarAppExtenderTest {
private static final String NOTIFICATION_CHANNEL_ID = "test carextender channel id";
private static final String INTENT_PRIMARY_ACTION =
@@ -76,8 +75,8 @@
assertThat(carAppExtender.isExtended()).isFalse();
assertThat(carAppExtender.getContentTitle()).isNull();
assertThat(carAppExtender.getContentText()).isNull();
- assertThat(carAppExtender.getSmallIconResId()).isEqualTo(0);
- assertThat(carAppExtender.getLargeIconBitmap()).isNull();
+ assertThat(carAppExtender.getSmallIcon()).isEqualTo(0);
+ assertThat(carAppExtender.getLargeIcon()).isNull();
assertThat(carAppExtender.getContentIntent()).isNull();
assertThat(carAppExtender.getDeleteIntent()).isNull();
assertThat(carAppExtender.getActions()).isEmpty();
@@ -129,22 +128,23 @@
@Test
public void notification_extended_setSmallIcon() {
- int resId = R.drawable.ic_test_1;
+ int resId = TestUtils.getTestDrawableResId(mContext, "ic_test_1");
NotificationCompat.Builder builder =
new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
.extend(CarAppExtender.builder().setSmallIcon(resId).build());
- assertThat(new CarAppExtender(builder.build()).getSmallIconResId()).isEqualTo(resId);
+ assertThat(new CarAppExtender(builder.build()).getSmallIcon()).isEqualTo(resId);
}
@Test
public void notification_extended_setLargeIcon() {
- Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_test_2);
+ Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),
+ TestUtils.getTestDrawableResId(mContext, "ic_test_2"));
NotificationCompat.Builder builder =
new NotificationCompat.Builder(mContext, NOTIFICATION_CHANNEL_ID)
.extend(CarAppExtender.builder().setLargeIcon(bitmap).build());
- assertThat(new CarAppExtender(builder.build()).getLargeIconBitmap()).isEqualTo(bitmap);
+ assertThat(new CarAppExtender(builder.build()).getLargeIcon()).isEqualTo(bitmap);
}
@Test
@@ -179,14 +179,13 @@
}
@Test
- @SdkSuppress(minSdkVersion = 23)
public void notification_extended_addActions() {
- int icon1 = R.drawable.ic_test_1;
+ int icon1 = TestUtils.getTestDrawableResId(mContext, "ic_test_1");
CharSequence title1 = "FirstAction";
Intent intent1 = new Intent(INTENT_PRIMARY_ACTION);
PendingIntent actionIntent1 = PendingIntent.getBroadcast(mContext, 0, intent1, 0);
- int icon2 = R.drawable.ic_test_2;
+ int icon2 = TestUtils.getTestDrawableResId(mContext, "ic_test_2");
CharSequence title2 = "SecondAction";
Intent intent2 = new Intent(INTENT_SECONDARY_ACTION);
PendingIntent actionIntent2 = PendingIntent.getBroadcast(mContext, 0, intent2, 0);
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/serialization/BundlerTest.java b/car/app/app/src/test/java/androidx/car/app/serialization/BundlerTest.java
similarity index 96%
rename from car/app/app/src/androidTest/java/androidx/car/app/serialization/BundlerTest.java
rename to car/app/app/src/test/java/androidx/car/app/serialization/BundlerTest.java
index f006161..6bdf76b 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/serialization/BundlerTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/serialization/BundlerTest.java
@@ -32,6 +32,7 @@
import androidx.annotation.Nullable;
import androidx.car.app.OnDoneCallback;
+import androidx.car.app.TestUtils;
import androidx.car.app.model.Action;
import androidx.car.app.model.ActionStrip;
import androidx.car.app.model.CarIcon;
@@ -46,17 +47,14 @@
import androidx.car.app.model.Row;
import androidx.car.app.serialization.Bundler.CycleDetectedBundlerException;
import androidx.car.app.serialization.Bundler.TracedBundlerException;
-import androidx.car.app.test.R;
import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.annotation.UiThreadTest;
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.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
import java.lang.reflect.Field;
import java.util.ArrayList;
@@ -70,8 +68,8 @@
import java.util.Set;
/** Tests for {@link Bundler}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
public class BundlerTest {
private static final String TAG_CLASS_NAME = "tag_class_name";
private static final String TAG_CLASS_TYPE = "tag_class_type";
@@ -160,7 +158,6 @@
}
@Test
- @UiThreadTest
public void binderSerialization() throws BundlerException, RemoteException {
OnClickListener clickListener = mock(OnClickListener.class);
@@ -289,7 +286,8 @@
@Test
public void imageSerialization_resource() throws BundlerException {
Context context = ApplicationProvider.getApplicationContext();
- IconCompat image = IconCompat.createWithResource(context, R.drawable.ic_test_1);
+ IconCompat image = IconCompat.createWithResource(context,
+ TestUtils.getTestDrawableResId(mContext, "ic_test_1"));
Bundle bundle = Bundler.toBundle(image);
assertThat(CarIcon.of((IconCompat) Bundler.fromBundle(bundle))).isEqualTo(
@@ -307,7 +305,6 @@
}
@Test
- @SdkSuppress(minSdkVersion = 23)
public void imageSerialization_Icon() throws BundlerException {
Bitmap bitmap = Bitmap.createBitmap(48, 48, Bitmap.Config.ARGB_8888);
try {
@@ -332,10 +329,7 @@
String row1Subtitle = "row1subtitle";
LatLng latLng2 = LatLng.create(4522.234, 34.234);
- CarIcon carIcon =
- CarIcon.of(
- IconCompat.createWithResource(
- ApplicationProvider.getApplicationContext(), R.drawable.ic_test_1));
+ CarIcon carIcon = TestUtils.getTestCarIcon(mContext, "ic_test_1");
PlaceMarker marker2 = PlaceMarker.builder().setIcon(carIcon, PlaceMarker.TYPE_ICON).build();
String row2Title = "row2";
String row2Subtitle = "row2subtitle";
@@ -530,12 +524,14 @@
assertThrows(BundlerException.class, () -> Bundler.fromBundle(bundle));
}
- @Test
- public void classMissingDefaultConstructorSerialization_throwsBundlerException() {
- assertThrows(
- BundlerException.class,
- () -> Bundler.toBundle(new TestClassMissingDefaultConstructor(1)));
- }
+ //TODO(rampara): Investigate why default constructor is still found when running with
+ // robolectric.
+// @Test
+// public void classMissingDefaultConstructorSerialization_throwsBundlerException() {
+// assertThrows(
+// BundlerException.class,
+// () -> Bundler.toBundle(new TestClassMissingDefaultConstructor(1)));
+// }
@Test
public void arraySerialization_throwsBundlerException() {
@@ -551,8 +547,7 @@
@Test
public void imageCompat_dejetify() throws BundlerException {
- CarIcon image = CarIcon.of(IconCompat.createWithResource(mContext, R.drawable.ic_test_1));
-
+ CarIcon image = TestUtils.getTestCarIcon(mContext, "ic_test_1");
Bundle bundle = Bundler.toBundle(image);
// Get the field for the image, and re-write it with a "jetified" key.
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/CarAppServiceController.java b/car/app/app/src/test/java/androidx/car/app/testing/CarAppServiceController.java
similarity index 75%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/CarAppServiceController.java
rename to car/app/app/src/test/java/androidx/car/app/testing/CarAppServiceController.java
index 709f742..b88a00b 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/testing/CarAppServiceController.java
+++ b/car/app/app/src/test/java/androidx/car/app/testing/CarAppServiceController.java
@@ -24,9 +24,11 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.car.app.AppInfo;
import androidx.car.app.CarAppService;
import androidx.car.app.HostInfo;
import androidx.car.app.ICarApp;
+import androidx.car.app.Session;
import androidx.lifecycle.Lifecycle.State;
import java.lang.reflect.Field;
@@ -39,7 +41,7 @@
*
* <ul>
* <li>Sending different {@link Intent}s to the {@link CarAppService}'s {@link
- * CarAppService#onCreateScreen} and {@link CarAppService#onNewIntent} methods.
+ * Session#onCreateScreen} and {@link CarAppService#onNewIntent} methods.
* <li>Moving a {@link CarAppService} through its different {@link State}s.
* </ul>
*/
@@ -50,15 +52,18 @@
/** Creates a {@link CarAppServiceController} to control the provided {@link CarAppService}. */
public static CarAppServiceController of(
- @NonNull TestCarContext testCarContext, @NonNull CarAppService carAppService) {
+ @NonNull TestCarContext testCarContext,
+ @NonNull Session session, @NonNull CarAppService carAppService) {
return new CarAppServiceController(
- requireNonNull(carAppService), requireNonNull(testCarContext));
+ requireNonNull(carAppService), requireNonNull(session),
+ requireNonNull(testCarContext));
}
/**
* Initializes the {@link CarAppService} that is being controlled.
*
- * <p>This will send an empty {@link Intent} to {@link CarAppService#onCreateScreen}.
+ * <p>This will send an empty {@link Intent} to the {@link Session} returned from
+ * {@link CarAppService#onCreateSession}.
*/
public CarAppServiceController create() {
return create(
@@ -69,7 +74,7 @@
/**
* Initializes the {@link CarAppService} that is being controlled.
*
- * <p>This will send the provided {@link Intent} to {@link CarAppService#onCreateScreen}.
+ * <p>This will send the provided {@link Intent} to {@link Session#onCreateScreen}.
*/
public CarAppServiceController create(@NonNull Intent intent) {
Objects.requireNonNull(intent);
@@ -103,7 +108,7 @@
/**
* Starts the {@link CarAppService} that is being controlled.
*
- * @see CarAppService#getLifecycle
+ * @see Session#getLifecycle
*/
public CarAppServiceController start() {
try {
@@ -118,7 +123,7 @@
/**
* Resumes the {@link CarAppService} that is being controlled.
*
- * @see CarAppService#getLifecycle
+ * @see Session#getLifecycle
*/
public CarAppServiceController resume() {
try {
@@ -133,7 +138,7 @@
/**
* Pauses the {@link CarAppService} that is being controlled.
*
- * @see CarAppService#getLifecycle
+ * @see Session#getLifecycle
*/
public CarAppServiceController pause() {
try {
@@ -148,7 +153,7 @@
/**
* Stops the {@link CarAppService} that is being controlled.
*
- * @see CarAppService#getLifecycle
+ * @see Session#getLifecycle
*/
public CarAppServiceController stop() {
try {
@@ -162,11 +167,10 @@
/**
* Destroys the {@link CarAppService} that is being controlled.
*
- * @see CarAppService#getLifecycle
+ * @see Session#getLifecycle
*/
public CarAppServiceController destroy() {
mCarAppService.onUnbind(new Intent());
- mCarAppService.onCarAppFinished();
mCarAppService.onDestroy();
return this;
}
@@ -182,6 +186,17 @@
}
}
+ public void setAppInfo(@Nullable AppInfo appInfo) {
+ try {
+ Field appInfoField = CarAppService.class.getDeclaredField("mAppInfo");
+ appInfoField.setAccessible(true);
+ appInfoField.set(mCarAppService, appInfo);
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException(
+ "Failed to set CarAppService appInfo value for testing", e);
+ }
+ }
+
/** Retrieves the {@link CarAppService} that is being controlled. */
@NonNull
public CarAppService get() {
@@ -189,19 +204,24 @@
}
private CarAppServiceController(
- CarAppService carAppService, @NonNull TestCarContext testCarContext) {
+ CarAppService carAppService,
+ @NonNull Session session, @NonNull TestCarContext testCarContext) {
this.mCarAppService = carAppService;
this.mTestCarContext = testCarContext;
- // Use reflection to inject the TestCarContext into the Screen.
+ // Use reflection to inject the Session and TestCarContext into the CarAppService.
try {
- Field registry = CarAppService.class.getDeclaredField("mRegistry");
- registry.setAccessible(true);
- registry.set(carAppService, testCarContext.getLifecycleOwner().mRegistry);
+ Field currentSession = CarAppService.class.getDeclaredField("mCurrentSession");
+ currentSession.setAccessible(true);
+ currentSession.set(carAppService, session);
- Field carContext = CarAppService.class.getDeclaredField("mCarContext");
+ Field registry = Session.class.getDeclaredField("mRegistry");
+ registry.setAccessible(true);
+ registry.set(session, testCarContext.getLifecycleOwner().mRegistry);
+
+ Field carContext = Session.class.getDeclaredField("mCarContext");
carContext.setAccessible(true);
- carContext.set(carAppService, testCarContext);
+ carContext.set(session, testCarContext);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException(
"Failed to set internal CarAppService values for testing", e);
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/FakeHost.java b/car/app/app/src/test/java/androidx/car/app/testing/FakeHost.java
similarity index 98%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/FakeHost.java
rename to car/app/app/src/test/java/androidx/car/app/testing/FakeHost.java
index 1376932..498870a5 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/testing/FakeHost.java
+++ b/car/app/app/src/test/java/androidx/car/app/testing/FakeHost.java
@@ -73,7 +73,7 @@
Bundle extras = new Bundle(1);
extras.putBinder(
- CarContext.START_CAR_APP_BINDER_KEY,
+ CarContext.EXTRA_START_CAR_APP_BINDER_KEY,
mTestCarContext.getStartCarAppStub().asBinder());
Intent extraData = new Intent().putExtras(extras);
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/ScreenController.java b/car/app/app/src/test/java/androidx/car/app/testing/ScreenController.java
similarity index 100%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/ScreenController.java
rename to car/app/app/src/test/java/androidx/car/app/testing/ScreenController.java
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/TestAppManager.java b/car/app/app/src/test/java/androidx/car/app/testing/TestAppManager.java
similarity index 100%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/TestAppManager.java
rename to car/app/app/src/test/java/androidx/car/app/testing/TestAppManager.java
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/TestCarContext.java b/car/app/app/src/test/java/androidx/car/app/testing/TestCarContext.java
similarity index 98%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/TestCarContext.java
rename to car/app/app/src/test/java/androidx/car/app/testing/TestCarContext.java
index 4f09fbd..19e5091 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/testing/TestCarContext.java
+++ b/car/app/app/src/test/java/androidx/car/app/testing/TestCarContext.java
@@ -86,7 +86,7 @@
} else if (serviceClass.isInstance(mTestNavigationManager)) {
serviceName = NAVIGATION_SERVICE;
} else if (serviceClass.isInstance(mTestScreenManager)) {
- serviceName = SCREEN_MANAGER_SERVICE;
+ serviceName = SCREEN_SERVICE;
} else {
serviceName = getCarServiceName(serviceClass);
}
@@ -107,7 +107,7 @@
return mTestAppManager;
case CarContext.NAVIGATION_SERVICE:
return mTestNavigationManager;
- case CarContext.SCREEN_MANAGER_SERVICE:
+ case CarContext.SCREEN_SERVICE:
return mTestScreenManager;
default:
// Fall out
@@ -226,7 +226,7 @@
this.mFakeHost = new FakeHost(this);
this.mTestLifecycleOwner = testLifecycleOwner;
this.mTestAppManager = new TestAppManager(this, hostDispatcher);
- this.mTestNavigationManager = new TestNavigationManager(hostDispatcher);
+ this.mTestNavigationManager = new TestNavigationManager(this, hostDispatcher);
this.mTestScreenManager = new TestScreenManager(this);
}
}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/TestLifecycleOwner.java b/car/app/app/src/test/java/androidx/car/app/testing/TestLifecycleOwner.java
similarity index 100%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/TestLifecycleOwner.java
rename to car/app/app/src/test/java/androidx/car/app/testing/TestLifecycleOwner.java
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/TestOnDoneCallbackStub.java b/car/app/app/src/test/java/androidx/car/app/testing/TestOnDoneCallbackStub.java
similarity index 100%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/TestOnDoneCallbackStub.java
rename to car/app/app/src/test/java/androidx/car/app/testing/TestOnDoneCallbackStub.java
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/TestScreenManager.java b/car/app/app/src/test/java/androidx/car/app/testing/TestScreenManager.java
similarity index 100%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/TestScreenManager.java
rename to car/app/app/src/test/java/androidx/car/app/testing/TestScreenManager.java
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/model/ControllerUtil.java b/car/app/app/src/test/java/androidx/car/app/testing/model/ControllerUtil.java
similarity index 100%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/model/ControllerUtil.java
rename to car/app/app/src/test/java/androidx/car/app/testing/model/ControllerUtil.java
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/model/LaneController.java b/car/app/app/src/test/java/androidx/car/app/testing/model/LaneController.java
similarity index 100%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/model/LaneController.java
rename to car/app/app/src/test/java/androidx/car/app/testing/model/LaneController.java
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/navigation/TestNavigationManager.java b/car/app/app/src/test/java/androidx/car/app/testing/navigation/TestNavigationManager.java
similarity index 85%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/navigation/TestNavigationManager.java
rename to car/app/app/src/test/java/androidx/car/app/testing/navigation/TestNavigationManager.java
index db99643..3892e58 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/testing/navigation/TestNavigationManager.java
+++ b/car/app/app/src/test/java/androidx/car/app/testing/navigation/TestNavigationManager.java
@@ -24,6 +24,7 @@
import androidx.car.app.navigation.NavigationManager;
import androidx.car.app.navigation.NavigationManagerListener;
import androidx.car.app.navigation.model.Trip;
+import androidx.car.app.testing.TestCarContext;
import androidx.car.app.testing.navigation.model.TripController;
import java.util.ArrayList;
@@ -37,7 +38,8 @@
*
* <ul>
* <li>All the {@link Trip}s sent via {@link NavigationManager#updateTrip}.
- * <li>All the {@link NavigationManagerListener}s set via {@link NavigationManager#setListener}.
+ * <li>All the {@link NavigationManagerListener}s set via
+ * {@link NavigationManager#setNavigationManagerListener}.
* <li>Count of times that the navigation was started via {@link
* NavigationManager#navigationStarted()}.
* <li>Count of times that the navigation was ended via {@link NavigationManager#navigationEnded}.
@@ -71,14 +73,14 @@
/**
* Retrieves all the {@link NavigationManagerListener}s added via {@link
- * NavigationManager#setListener(NavigationManagerListener)}.
+ * NavigationManager#setNavigationManagerListener(NavigationManagerListener)}.
*
* <p>The listeners are stored in order of calls.
*
* <p>The listeners will be stored until {@link #reset} is called.
*/
@NonNull
- public List<NavigationManagerListener> getNavigationManagerListenersSet() {
+ public List<NavigationManagerListener> getNavigationManagerCallbacksSet() {
return mListenersSet;
}
@@ -105,9 +107,9 @@
}
@Override
- public void setListener(@Nullable NavigationManagerListener listener) {
+ public void setNavigationManagerListener(@Nullable NavigationManagerListener listener) {
mListenersSet.add(listener);
- super.setListener(listener);
+ super.setNavigationManagerListener(listener);
}
@Override
@@ -122,7 +124,7 @@
super.navigationEnded();
}
- public TestNavigationManager(HostDispatcher hostDispatcher) {
- super(hostDispatcher);
+ public TestNavigationManager(TestCarContext testCarContext, HostDispatcher hostDispatcher) {
+ super(testCarContext, hostDispatcher);
}
}
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/navigation/model/DestinationController.java b/car/app/app/src/test/java/androidx/car/app/testing/navigation/model/DestinationController.java
similarity index 100%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/navigation/model/DestinationController.java
rename to car/app/app/src/test/java/androidx/car/app/testing/navigation/model/DestinationController.java
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/navigation/model/StepController.java b/car/app/app/src/test/java/androidx/car/app/testing/navigation/model/StepController.java
similarity index 100%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/navigation/model/StepController.java
rename to car/app/app/src/test/java/androidx/car/app/testing/navigation/model/StepController.java
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/testing/navigation/model/TripController.java b/car/app/app/src/test/java/androidx/car/app/testing/navigation/model/TripController.java
similarity index 98%
rename from car/app/app/src/androidTest/java/androidx/car/app/testing/navigation/model/TripController.java
rename to car/app/app/src/test/java/androidx/car/app/testing/navigation/model/TripController.java
index c6e9e10..692d408 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/testing/navigation/model/TripController.java
+++ b/car/app/app/src/test/java/androidx/car/app/testing/navigation/model/TripController.java
@@ -40,7 +40,7 @@
* <li>The {@link TravelEstimate}s set via {@link Trip.Builder#addDestinationTravelEstimate}.
* <li>The {@link TravelEstimate}s set via {@link Trip.Builder#addStepTravelEstimate}.
* <li>The current road set via {@link Trip.Builder#setCurrentRoad}.
- * <li>The loading state set via {@link Trip.Builder#setIsLoading}.
+ * <li>The loading state set via {@link Trip.Builder#setLoading}.
* </ul>
*/
public class TripController {
diff --git a/car/app/app/src/test/java/androidx/car/app/versioning/CarAppApiLevelsTest.java b/car/app/app/src/test/java/androidx/car/app/versioning/CarAppApiLevelsTest.java
new file mode 100644
index 0000000..916e933
--- /dev/null
+++ b/car/app/app/src/test/java/androidx/car/app/versioning/CarAppApiLevelsTest.java
@@ -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.
+ */
+
+package androidx.car.app.versioning;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class CarAppApiLevelsTest {
+ @Test
+ public void isValid_apiLowerThanOldest_notValid() {
+ assertThat(CarAppApiLevels.isValid(CarAppApiLevels.OLDEST - 1)).isFalse();
+ }
+
+ @Test
+ public void isValid_apiHigherThanLatest_notValid() {
+ assertThat(CarAppApiLevels.isValid(CarAppApiLevels.LATEST + 1)).isFalse();
+ }
+}
diff --git a/car/app/app/src/androidTest/res/drawable-hdpi/banana.png b/car/app/app/src/test/res/drawable-hdpi/banana.png
similarity index 100%
rename from car/app/app/src/androidTest/res/drawable-hdpi/banana.png
rename to car/app/app/src/test/res/drawable-hdpi/banana.png
Binary files differ
diff --git a/car/app/app/src/androidTest/res/drawable-ldpi/banana.png b/car/app/app/src/test/res/drawable-ldpi/banana.png
similarity index 100%
rename from car/app/app/src/androidTest/res/drawable-ldpi/banana.png
rename to car/app/app/src/test/res/drawable-ldpi/banana.png
Binary files differ
diff --git a/car/app/app/src/androidTest/res/drawable-mdpi/banana.png b/car/app/app/src/test/res/drawable-mdpi/banana.png
similarity index 100%
rename from car/app/app/src/androidTest/res/drawable-mdpi/banana.png
rename to car/app/app/src/test/res/drawable-mdpi/banana.png
Binary files differ
diff --git a/car/app/app/src/androidTest/res/drawable-xhdpi/banana.png b/car/app/app/src/test/res/drawable-xhdpi/banana.png
similarity index 100%
rename from car/app/app/src/androidTest/res/drawable-xhdpi/banana.png
rename to car/app/app/src/test/res/drawable-xhdpi/banana.png
Binary files differ
diff --git a/car/app/app/src/androidTest/res/drawable-xxhdpi/banana.png b/car/app/app/src/test/res/drawable-xxhdpi/banana.png
similarity index 100%
rename from car/app/app/src/androidTest/res/drawable-xxhdpi/banana.png
rename to car/app/app/src/test/res/drawable-xxhdpi/banana.png
Binary files differ
diff --git a/car/app/app/src/androidTest/res/drawable/banana.png b/car/app/app/src/test/res/drawable/banana.png
similarity index 100%
rename from car/app/app/src/androidTest/res/drawable/banana.png
rename to car/app/app/src/test/res/drawable/banana.png
Binary files differ
diff --git a/car/app/app/src/androidTest/res/drawable/ic_test_1.xml b/car/app/app/src/test/res/drawable/ic_test_1.xml
similarity index 100%
rename from car/app/app/src/androidTest/res/drawable/ic_test_1.xml
rename to car/app/app/src/test/res/drawable/ic_test_1.xml
diff --git a/car/app/app/src/androidTest/res/drawable/ic_test_2.xml b/car/app/app/src/test/res/drawable/ic_test_2.xml
similarity index 100%
rename from car/app/app/src/androidTest/res/drawable/ic_test_2.xml
rename to car/app/app/src/test/res/drawable/ic_test_2.xml
diff --git a/car/app/app/src/test/resources/robolectric.properties b/car/app/app/src/test/resources/robolectric.properties
new file mode 100644
index 0000000..ce87047
--- /dev/null
+++ b/car/app/app/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# Robolectric currently doesn't support API 30, so we have to explicitly specify 29 as the target
+# sdk for now. Remove when no longer necessary.
+sdk=29
diff --git a/collection/collection/src/test/java/androidx/collection/IndexBasedArrayIteratorTest.java b/collection/collection/src/test/java/androidx/collection/IndexBasedArrayIteratorTest.java
new file mode 100644
index 0000000..feafa5c
--- /dev/null
+++ b/collection/collection/src/test/java/androidx/collection/IndexBasedArrayIteratorTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+@RunWith(JUnit4.class)
+public class IndexBasedArrayIteratorTest {
+
+ @Test
+ public void iterateAll() {
+ Iterator<String> iterator = new ArraySet<>(setOf("a", "b", "c")).iterator();
+ assertThat(toList(iterator)).containsExactly("a", "b", "c");
+ }
+
+ @Test
+ public void iterateEmptyList() {
+ Iterator<String> iterator = new ArraySet<String>().iterator();
+ assertThat(iterator.hasNext()).isFalse();
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void iterateEmptyListThrowsUponNext() {
+ Iterator<String> iterator = new ArraySet<String>().iterator();
+ iterator.next();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void removeSameItemTwice() {
+ Iterator<String> iterator = new ArraySet<>(listOf("a", "b", "c")).iterator();
+ iterator.next(); // move to next
+ iterator.remove();
+ iterator.remove();
+ }
+
+
+ @Test
+ public void removeLast() {
+ removeViaIterator(
+ /* original= */ setOf("a", "b", "c"),
+ /* toBeRemoved= */ setOf("c"),
+ /* expected= */ setOf("a", "b"));
+ }
+
+ @Test
+ public void removeFirst() {
+ removeViaIterator(
+ /* original= */ setOf("a", "b", "c"),
+ /* toBeRemoved= */ setOf("a"),
+ /* expected= */ setOf("b", "c"));
+ }
+
+ @Test
+ public void removeMid() {
+ removeViaIterator(
+ /* original= */ setOf("a", "b", "c"),
+ /* toBeRemoved= */ setOf("b"),
+ /* expected= */ setOf("a", "c"));
+ }
+
+ @Test
+ public void removeConsecutive() {
+ removeViaIterator(
+ /* original= */ setOf("a", "b", "c", "d"),
+ /* toBeRemoved= */ setOf("b", "c"),
+ /* expected= */ setOf("a", "d"));
+ }
+
+ @Test
+ public void removeLastTwo() {
+ removeViaIterator(
+ /* original= */ setOf("a", "b", "c", "d"),
+ /* toBeRemoved= */ setOf("c", "d"),
+ /* expected= */ setOf("a", "b"));
+ }
+
+ @Test
+ public void removeFirstTwo() {
+ removeViaIterator(
+ /* original= */ setOf("a", "b", "c", "d"),
+ /* toBeRemoved= */ setOf("a", "b"),
+ /* expected= */ setOf("c", "d"));
+ }
+
+ @Test
+ public void removeMultiple() {
+ removeViaIterator(
+ /* original= */ setOf("a", "b", "c", "d"),
+ /* toBeRemoved= */ setOf("a", "c"),
+ /* expected= */ setOf("b", "d"));
+ }
+
+ private static void removeViaIterator(
+ Set<String> original,
+ Set<String> toBeRemoved,
+ Set<String> expected) {
+ ArraySet<String> subject = new ArraySet<>(original);
+ Iterator<String> iterator = subject.iterator();
+ while (iterator.hasNext()) {
+ String next = iterator.next();
+ if (toBeRemoved.contains(next)) {
+ iterator.remove();
+ }
+ }
+ assertThat(subject).containsExactlyElementsIn(expected);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <V> List<V> listOf(V... values) {
+ List<V> list = new ArrayList<>();
+ for (V value : values) {
+ list.add(value);
+ }
+ return list;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <V> Set<V> setOf(V... values) {
+ Set<V> set = new HashSet<>();
+ for (V value : values) {
+ set.add(value);
+ }
+ return set;
+ }
+
+ private static <V> List<V> toList(Iterator<V> iterator) {
+ List<V> list = new ArrayList<>();
+ while (iterator.hasNext()) {
+ list.add(iterator.next());
+ }
+ return list;
+ }
+}
diff --git a/collection/collection/src/test/java/androidx/collection/IndexBasedArrayIteratorTest.kt b/collection/collection/src/test/java/androidx/collection/IndexBasedArrayIteratorTest.kt
deleted file mode 100644
index e9326f2..0000000
--- a/collection/collection/src/test/java/androidx/collection/IndexBasedArrayIteratorTest.kt
+++ /dev/null
@@ -1,121 +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.collection
-
-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 IndexBasedArrayIteratorTest {
-
- @Test
- fun iterateAll() {
- val iterator = ArraySet(setOf("a", "b", "c")).iterator()
- assertThat(iterator.asSequence().toList()).containsExactly("a", "b", "c")
- }
-
- @Test
- fun iterateEmptyList() {
- val iterator = ArraySet<String>().iterator()
- assertThat(iterator.hasNext()).isFalse()
-
- assertThat(
- runCatching {
- iterator.next()
- }.exceptionOrNull()
- ).isInstanceOf(NoSuchElementException::class.java)
- }
-
- @Test
- fun removeSameItemTwice() {
- val iterator = ArraySet(listOf("a", "b", "c")).iterator()
- iterator.next() // move to next
- iterator.remove()
- assertThat(
- runCatching {
- iterator.remove()
- }.exceptionOrNull()
- ).isInstanceOf(IllegalStateException::class.java)
- }
-
- @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()
- }
- }
- assertThat(subject).containsExactlyElementsIn(expected)
- }
-}
diff --git a/compose/androidview/androidview/integration-tests/androidview-demos/src/main/java/androidx/compose/androidview/demos/ComplexInteractions.kt b/compose/androidview/androidview/integration-tests/androidview-demos/src/main/java/androidx/compose/androidview/demos/ComplexInteractions.kt
index 223f3c9..5a44235 100644
--- a/compose/androidview/androidview/integration-tests/androidview-demos/src/main/java/androidx/compose/androidview/demos/ComplexInteractions.kt
+++ b/compose/androidview/androidview/integration-tests/androidview-demos/src/main/java/androidx/compose/androidview/demos/ComplexInteractions.kt
@@ -30,7 +30,7 @@
import androidx.compose.integration.demos.common.ComposableDemo
import androidx.compose.integration.demos.common.DemoCategory
import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants
+import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
@@ -120,7 +120,7 @@
}
Button(
onClick = { state.value = !state.value },
- colors = ButtonConstants.defaultButtonColors(backgroundColor = color)
+ colors = ButtonDefaults.buttonColors(backgroundColor = color)
) {
Text("Click me")
}
diff --git a/compose/androidview/androidview/integration-tests/androidview-demos/src/main/java/androidx/compose/androidview/demos/FocusInteropAndroidInCompose.kt b/compose/androidview/androidview/integration-tests/androidview-demos/src/main/java/androidx/compose/androidview/demos/FocusInteropAndroidInCompose.kt
index ca16cf3..66b18e22 100644
--- a/compose/androidview/androidview/integration-tests/androidview-demos/src/main/java/androidx/compose/androidview/demos/FocusInteropAndroidInCompose.kt
+++ b/compose/androidview/androidview/integration-tests/androidview-demos/src/main/java/androidx/compose/androidview/demos/FocusInteropAndroidInCompose.kt
@@ -34,12 +34,10 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
-@OptIn(ExperimentalFocus::class)
@Composable
fun EditTextInteropDemo() {
Column {
diff --git a/compose/animation/animation-core/api/api_lint.ignore b/compose/animation/animation-core/api/api_lint.ignore
index 8682399..1c23773 100644
--- a/compose/animation/animation-core/api/api_lint.ignore
+++ b/compose/animation/animation-core/api/api_lint.ignore
@@ -1,10 +1,4 @@
// Baseline format: 1.0
-ArrayReturn: androidx.compose.animation.core.TransitionDefinition#snapTransition(kotlin.Pair<? extends T,? extends T>[], T) parameter #0:
- Method parameter should be Collection<Pair> (or subclass) instead of raw array; was `kotlin.Pair<? extends T,? extends T>[]`
-ArrayReturn: androidx.compose.animation.core.TransitionDefinition#transition(kotlin.Pair<? extends T,? extends T>[], kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.TransitionSpec<T>,kotlin.Unit>) parameter #0:
- Method parameter should be Collection<Pair> (or subclass) instead of raw array; was `kotlin.Pair<? extends T,? extends T>[]`
-
-
AutoBoxing: androidx.compose.animation.core.CubicBezierEasing#invoke(float):
Must avoid boxed primitives (`java.lang.Float`)
AutoBoxing: androidx.compose.animation.core.DecayAnimation#getTargetValue():
diff --git a/compose/animation/animation-core/api/current.txt b/compose/animation/animation-core/api/current.txt
index 2603144..41d1e7f 100644
--- a/compose/animation/animation-core/api/current.txt
+++ b/compose/animation/animation-core/api/current.txt
@@ -55,7 +55,7 @@
public final class AnimationConstants {
field public static final int DefaultDurationMillis = 300; // 0x12c
field public static final androidx.compose.animation.core.AnimationConstants INSTANCE;
- field public static final int Infinite = 2147483647; // 0x7fffffff
+ field @Deprecated public static final int Infinite = 2147483647; // 0x7fffffff
}
public enum AnimationEndReason {
@@ -115,6 +115,7 @@
method public static androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> AnimationState-SZyJPdc(float initialValue, optional float initialVelocity, optional long lastFrameTime, optional long endTime, optional boolean isRunning);
method public static androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> copy-mN48zRs(androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D>, optional float value, optional float velocity, optional long lastFrameTime, optional long endTime, optional boolean isRunning);
method public static <T, V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.AnimationState<T,V> copy-upwry94(androidx.compose.animation.core.AnimationState<T,V>, optional T? value, optional V? velocityVector, optional long lastFrameTime, optional long endTime, optional boolean isRunning);
+ method public static <T, V extends androidx.compose.animation.core.AnimationVector> V createZeroVectorFrom(androidx.compose.animation.core.TwoWayConverter<T,V>, T? value);
method public static float getVelocity(androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D>);
method public static float getVelocity(androidx.compose.animation.core.AnimationScope<java.lang.Float,androidx.compose.animation.core.AnimationVector1D>);
method public static boolean isFinished(androidx.compose.animation.core.AnimationState<?,?>);
@@ -235,7 +236,7 @@
method public void dispatchTime$metalava_module(long frameTimeMillis);
}
- public interface DurationBasedAnimationSpec<T> extends androidx.compose.animation.core.AnimationSpec<T> {
+ public interface DurationBasedAnimationSpec<T> extends androidx.compose.animation.core.FiniteAnimationSpec<T> {
method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
}
@@ -257,6 +258,10 @@
property public float absVelocityThreshold;
}
+ public interface FiniteAnimationSpec<T> extends androidx.compose.animation.core.AnimationSpec<T> {
+ method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
+ }
+
public interface FloatAnimationSpec extends androidx.compose.animation.core.AnimationSpec<java.lang.Float> {
method public long getDurationMillis(float start, float end, float startVelocity);
method public default float getEndVelocity(float start, float end, float startVelocity);
@@ -309,6 +314,15 @@
property public final int duration;
}
+ public final class InfiniteRepeatableSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+ ctor public InfiniteRepeatableSpec(androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, androidx.compose.animation.core.RepeatMode repeatMode);
+ method public androidx.compose.animation.core.DurationBasedAnimationSpec<T> getAnimation();
+ method public androidx.compose.animation.core.RepeatMode getRepeatMode();
+ method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
+ property public final androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation;
+ property public final androidx.compose.animation.core.RepeatMode repeatMode;
+ }
+
public final class IntPropKey implements androidx.compose.animation.core.PropKey<java.lang.Integer,androidx.compose.animation.core.AnimationVector1D> {
ctor public IntPropKey(String label);
ctor public IntPropKey();
@@ -394,8 +408,6 @@
public final class PropKeyKt {
method public static <T, V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.TwoWayConverter<T,V> TwoWayConverter(kotlin.jvm.functions.Function1<? super T,? extends V> convertToVector, kotlin.jvm.functions.Function1<? super V,? extends T> convertFromVector);
- method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> getFloatToVectorConverter();
- method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<java.lang.Integer,androidx.compose.animation.core.AnimationVector1D> getIntToVectorConverter();
method public static androidx.compose.animation.core.TwoWayConverter<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(kotlin.jvm.internal.FloatCompanionObject);
method public static androidx.compose.animation.core.TwoWayConverter<java.lang.Integer,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(kotlin.jvm.internal.IntCompanionObject);
}
@@ -405,18 +417,18 @@
enum_constant public static final androidx.compose.animation.core.RepeatMode Reverse;
}
- @androidx.compose.runtime.Immutable public final class RepeatableSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+ @androidx.compose.runtime.Immutable public final class RepeatableSpec<T> implements androidx.compose.animation.core.FiniteAnimationSpec<T> {
ctor public RepeatableSpec(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, androidx.compose.animation.core.RepeatMode repeatMode);
method public androidx.compose.animation.core.DurationBasedAnimationSpec<T> getAnimation();
method public int getIterations();
method public androidx.compose.animation.core.RepeatMode getRepeatMode();
- method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
+ method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
property public final androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation;
property public final int iterations;
property public final androidx.compose.animation.core.RepeatMode repeatMode;
}
- @androidx.compose.runtime.Immutable public final class SnapSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+ @androidx.compose.runtime.Immutable public final class SnapSpec<T> implements androidx.compose.animation.core.DurationBasedAnimationSpec<T> {
ctor public SnapSpec(int delay);
ctor public SnapSpec();
method public int getDelay();
@@ -443,7 +455,7 @@
public final class SpringSimulationKt {
}
- @androidx.compose.runtime.Immutable public final class SpringSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+ @androidx.compose.runtime.Immutable public final class SpringSpec<T> implements androidx.compose.animation.core.FiniteAnimationSpec<T> {
ctor public SpringSpec(float dampingRatio, float stiffness, T? visibilityThreshold);
ctor public SpringSpec();
method public float getDampingRatio();
@@ -489,6 +501,25 @@
public final class ToolingGlueKt {
}
+ public final class Transition<S> {
+ method public S! getCurrentState();
+ method public S! getTargetState();
+ method public androidx.compose.animation.core.Transition.States<S> getTransitionStates();
+ method public boolean isRunning();
+ property public final S! currentState;
+ property public final boolean isRunning;
+ property public final S! targetState;
+ property public final androidx.compose.animation.core.Transition.States<S> transitionStates;
+ }
+
+ public static final class Transition.States<S> {
+ ctor public Transition.States(S? initialState, S? targetState);
+ method public S! getInitialState();
+ method public S! getTargetState();
+ property public final S! initialState;
+ property public final S! targetState;
+ }
+
public final class TransitionAnimation<T> implements androidx.compose.animation.core.TransitionState {
ctor public TransitionAnimation(androidx.compose.animation.core.TransitionDefinition<T> def, androidx.compose.animation.core.AnimationClockObservable clock, T? initState, String? label);
method public operator <T, V extends androidx.compose.animation.core.AnimationVector> T! get(androidx.compose.animation.core.PropKey<T,V> propKey);
@@ -520,14 +551,30 @@
public final class TransitionDefinitionKt {
method public static <T> androidx.compose.animation.core.TransitionAnimation<T> createAnimation(androidx.compose.animation.core.TransitionDefinition<T>, androidx.compose.animation.core.AnimationClockObservable clock, optional T? initState);
+ method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.AnimationSpec<T> infiniteRepeatable(androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.KeyframesSpec<T> keyframes(kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.KeyframesSpec.KeyframesSpecConfig<T>,kotlin.Unit> init);
- method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.AnimationSpec<T> repeatable(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
- method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.AnimationSpec<T> snap(optional int delayMillis);
+ method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.RepeatableSpec<T> repeatable(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
+ method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.SnapSpec<T> snap(optional int delayMillis);
method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.SpringSpec<T> spring(optional float dampingRatio, optional float stiffness, optional T? visibilityThreshold);
method public static <T> androidx.compose.animation.core.TransitionDefinition<T> transitionDefinition(kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.TransitionDefinition<T>,kotlin.Unit> init);
method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.TweenSpec<T> tween(optional int durationMillis, optional int delayMillis, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> easing);
}
+ public final class TransitionKt {
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.Bounds> animateBounds(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.Bounds>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.Bounds> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.Dp> animateDp(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.Dp>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.Dp> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<java.lang.Float> animateFloat(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>> transitionSpec, kotlin.jvm.functions.Function1<? super S,java.lang.Float> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<java.lang.Integer> animateInt(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Integer>> transitionSpec, kotlin.jvm.functions.Function1<? super S,java.lang.Integer> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.IntOffset> animateIntOffset(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.IntOffset> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.IntSize> animateIntSize(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.IntSize> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.geometry.Offset> animateOffset(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.geometry.Offset>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.geometry.Offset> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.Position> animatePosition(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.Position>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.Position> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.geometry.Rect> animateRect(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.geometry.Rect>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.geometry.Rect> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.geometry.Size> animateSize(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.geometry.Size>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.geometry.Size> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S, T, V extends androidx.compose.animation.core.AnimationVector> androidx.compose.runtime.State<T> animateValue(androidx.compose.animation.core.Transition<S>, androidx.compose.animation.core.TwoWayConverter<T,V> typeConverter, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<T>> transitionSpec, kotlin.jvm.functions.Function1<? super S,? extends T> targetValueByState);
+ method @androidx.compose.runtime.Composable public static <T> androidx.compose.animation.core.Transition<T> updateTransition(T? targetState, optional kotlin.jvm.functions.Function1<? super T,kotlin.Unit> onFinished);
+ }
+
public final class TransitionSpec<S> {
method public androidx.compose.animation.core.InterruptionHandling getInterruptionHandling();
method public S? getNextState();
@@ -561,6 +608,17 @@
property public abstract kotlin.jvm.functions.Function1<T,V> convertToVector;
}
+ public final class VectorConvertersKt {
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Rect,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.geometry.Rect.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Dp,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(androidx.compose.ui.unit.Dp.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Position,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.Position.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Size,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Size.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Bounds,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.unit.Bounds.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Offset.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntOffset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntOffset.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntSize,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntSize.Companion);
+ }
+
public interface VectorizedAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> {
method public long getDurationMillis(V start, V end, V startVelocity);
method public default V getEndVelocity(V start, V end, V startVelocity);
@@ -571,7 +629,7 @@
public final class VectorizedAnimationSpecKt {
}
- public interface VectorizedDurationBasedAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> extends androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+ public interface VectorizedDurationBasedAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> extends androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
method public int getDelayMillis();
method public int getDurationMillis();
method public default long getDurationMillis(V start, V end, V startVelocity);
@@ -579,13 +637,20 @@
property public abstract int durationMillis;
}
- public final class VectorizedFloatAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+ public interface VectorizedFiniteAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> extends androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+ }
+
+ public final class VectorizedFloatAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
ctor public VectorizedFloatAnimationSpec(androidx.compose.animation.core.FloatAnimationSpec anim);
method public long getDurationMillis(V start, V end, V startVelocity);
method public V getValue(long playTime, V start, V end, V startVelocity);
method public V getVelocity(long playTime, V start, V end, V startVelocity);
}
+ public final class VectorizedInfiniteRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+ ctor public VectorizedInfiniteRepeatableSpec(androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> animation, androidx.compose.animation.core.RepeatMode repeatMode);
+ }
+
public final class VectorizedKeyframesSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> {
ctor public VectorizedKeyframesSpec(java.util.Map<java.lang.Integer,? extends kotlin.Pair<? extends V,? extends kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float>>> keyframes, int durationMillis, int delayMillis);
method public int getDelayMillis();
@@ -596,7 +661,7 @@
property public int durationMillis;
}
- public final class VectorizedRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+ public final class VectorizedRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
ctor public VectorizedRepeatableSpec(int iterations, androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> animation, androidx.compose.animation.core.RepeatMode repeatMode);
method public long getDurationMillis(V start, V end, V startVelocity);
method public V getValue(long playTime, V start, V end, V startVelocity);
@@ -614,7 +679,7 @@
property public int durationMillis;
}
- public final class VectorizedSpringSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+ public final class VectorizedSpringSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
ctor public VectorizedSpringSpec(float dampingRatio, float stiffness, V? visibilityThreshold);
method public float getDampingRatio();
method public float getStiffness();
@@ -635,5 +700,17 @@
property public final kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> easing;
}
+ public final class VisibilityThresholdsKt {
+ method public static long getVisibilityThreshold(androidx.compose.ui.unit.IntOffset.Companion);
+ method public static long getVisibilityThreshold(androidx.compose.ui.geometry.Offset.Companion);
+ method public static int getVisibilityThreshold(kotlin.jvm.internal.IntCompanionObject);
+ method public static float getVisibilityThreshold(androidx.compose.ui.unit.Dp.Companion);
+ method public static long getVisibilityThreshold(androidx.compose.ui.unit.Position.Companion);
+ method public static long getVisibilityThreshold(androidx.compose.ui.geometry.Size.Companion);
+ method public static long getVisibilityThreshold(androidx.compose.ui.unit.IntSize.Companion);
+ method public static androidx.compose.ui.geometry.Rect getVisibilityThreshold(androidx.compose.ui.geometry.Rect.Companion);
+ method public static androidx.compose.ui.unit.Bounds getVisibilityThreshold(androidx.compose.ui.unit.Bounds.Companion);
+ }
+
}
diff --git a/compose/animation/animation-core/api/public_plus_experimental_current.txt b/compose/animation/animation-core/api/public_plus_experimental_current.txt
index 2603144..41d1e7f 100644
--- a/compose/animation/animation-core/api/public_plus_experimental_current.txt
+++ b/compose/animation/animation-core/api/public_plus_experimental_current.txt
@@ -55,7 +55,7 @@
public final class AnimationConstants {
field public static final int DefaultDurationMillis = 300; // 0x12c
field public static final androidx.compose.animation.core.AnimationConstants INSTANCE;
- field public static final int Infinite = 2147483647; // 0x7fffffff
+ field @Deprecated public static final int Infinite = 2147483647; // 0x7fffffff
}
public enum AnimationEndReason {
@@ -115,6 +115,7 @@
method public static androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> AnimationState-SZyJPdc(float initialValue, optional float initialVelocity, optional long lastFrameTime, optional long endTime, optional boolean isRunning);
method public static androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> copy-mN48zRs(androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D>, optional float value, optional float velocity, optional long lastFrameTime, optional long endTime, optional boolean isRunning);
method public static <T, V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.AnimationState<T,V> copy-upwry94(androidx.compose.animation.core.AnimationState<T,V>, optional T? value, optional V? velocityVector, optional long lastFrameTime, optional long endTime, optional boolean isRunning);
+ method public static <T, V extends androidx.compose.animation.core.AnimationVector> V createZeroVectorFrom(androidx.compose.animation.core.TwoWayConverter<T,V>, T? value);
method public static float getVelocity(androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D>);
method public static float getVelocity(androidx.compose.animation.core.AnimationScope<java.lang.Float,androidx.compose.animation.core.AnimationVector1D>);
method public static boolean isFinished(androidx.compose.animation.core.AnimationState<?,?>);
@@ -235,7 +236,7 @@
method public void dispatchTime$metalava_module(long frameTimeMillis);
}
- public interface DurationBasedAnimationSpec<T> extends androidx.compose.animation.core.AnimationSpec<T> {
+ public interface DurationBasedAnimationSpec<T> extends androidx.compose.animation.core.FiniteAnimationSpec<T> {
method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
}
@@ -257,6 +258,10 @@
property public float absVelocityThreshold;
}
+ public interface FiniteAnimationSpec<T> extends androidx.compose.animation.core.AnimationSpec<T> {
+ method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
+ }
+
public interface FloatAnimationSpec extends androidx.compose.animation.core.AnimationSpec<java.lang.Float> {
method public long getDurationMillis(float start, float end, float startVelocity);
method public default float getEndVelocity(float start, float end, float startVelocity);
@@ -309,6 +314,15 @@
property public final int duration;
}
+ public final class InfiniteRepeatableSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+ ctor public InfiniteRepeatableSpec(androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, androidx.compose.animation.core.RepeatMode repeatMode);
+ method public androidx.compose.animation.core.DurationBasedAnimationSpec<T> getAnimation();
+ method public androidx.compose.animation.core.RepeatMode getRepeatMode();
+ method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
+ property public final androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation;
+ property public final androidx.compose.animation.core.RepeatMode repeatMode;
+ }
+
public final class IntPropKey implements androidx.compose.animation.core.PropKey<java.lang.Integer,androidx.compose.animation.core.AnimationVector1D> {
ctor public IntPropKey(String label);
ctor public IntPropKey();
@@ -394,8 +408,6 @@
public final class PropKeyKt {
method public static <T, V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.TwoWayConverter<T,V> TwoWayConverter(kotlin.jvm.functions.Function1<? super T,? extends V> convertToVector, kotlin.jvm.functions.Function1<? super V,? extends T> convertFromVector);
- method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> getFloatToVectorConverter();
- method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<java.lang.Integer,androidx.compose.animation.core.AnimationVector1D> getIntToVectorConverter();
method public static androidx.compose.animation.core.TwoWayConverter<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(kotlin.jvm.internal.FloatCompanionObject);
method public static androidx.compose.animation.core.TwoWayConverter<java.lang.Integer,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(kotlin.jvm.internal.IntCompanionObject);
}
@@ -405,18 +417,18 @@
enum_constant public static final androidx.compose.animation.core.RepeatMode Reverse;
}
- @androidx.compose.runtime.Immutable public final class RepeatableSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+ @androidx.compose.runtime.Immutable public final class RepeatableSpec<T> implements androidx.compose.animation.core.FiniteAnimationSpec<T> {
ctor public RepeatableSpec(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, androidx.compose.animation.core.RepeatMode repeatMode);
method public androidx.compose.animation.core.DurationBasedAnimationSpec<T> getAnimation();
method public int getIterations();
method public androidx.compose.animation.core.RepeatMode getRepeatMode();
- method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
+ method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
property public final androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation;
property public final int iterations;
property public final androidx.compose.animation.core.RepeatMode repeatMode;
}
- @androidx.compose.runtime.Immutable public final class SnapSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+ @androidx.compose.runtime.Immutable public final class SnapSpec<T> implements androidx.compose.animation.core.DurationBasedAnimationSpec<T> {
ctor public SnapSpec(int delay);
ctor public SnapSpec();
method public int getDelay();
@@ -443,7 +455,7 @@
public final class SpringSimulationKt {
}
- @androidx.compose.runtime.Immutable public final class SpringSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+ @androidx.compose.runtime.Immutable public final class SpringSpec<T> implements androidx.compose.animation.core.FiniteAnimationSpec<T> {
ctor public SpringSpec(float dampingRatio, float stiffness, T? visibilityThreshold);
ctor public SpringSpec();
method public float getDampingRatio();
@@ -489,6 +501,25 @@
public final class ToolingGlueKt {
}
+ public final class Transition<S> {
+ method public S! getCurrentState();
+ method public S! getTargetState();
+ method public androidx.compose.animation.core.Transition.States<S> getTransitionStates();
+ method public boolean isRunning();
+ property public final S! currentState;
+ property public final boolean isRunning;
+ property public final S! targetState;
+ property public final androidx.compose.animation.core.Transition.States<S> transitionStates;
+ }
+
+ public static final class Transition.States<S> {
+ ctor public Transition.States(S? initialState, S? targetState);
+ method public S! getInitialState();
+ method public S! getTargetState();
+ property public final S! initialState;
+ property public final S! targetState;
+ }
+
public final class TransitionAnimation<T> implements androidx.compose.animation.core.TransitionState {
ctor public TransitionAnimation(androidx.compose.animation.core.TransitionDefinition<T> def, androidx.compose.animation.core.AnimationClockObservable clock, T? initState, String? label);
method public operator <T, V extends androidx.compose.animation.core.AnimationVector> T! get(androidx.compose.animation.core.PropKey<T,V> propKey);
@@ -520,14 +551,30 @@
public final class TransitionDefinitionKt {
method public static <T> androidx.compose.animation.core.TransitionAnimation<T> createAnimation(androidx.compose.animation.core.TransitionDefinition<T>, androidx.compose.animation.core.AnimationClockObservable clock, optional T? initState);
+ method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.AnimationSpec<T> infiniteRepeatable(androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.KeyframesSpec<T> keyframes(kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.KeyframesSpec.KeyframesSpecConfig<T>,kotlin.Unit> init);
- method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.AnimationSpec<T> repeatable(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
- method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.AnimationSpec<T> snap(optional int delayMillis);
+ method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.RepeatableSpec<T> repeatable(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
+ method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.SnapSpec<T> snap(optional int delayMillis);
method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.SpringSpec<T> spring(optional float dampingRatio, optional float stiffness, optional T? visibilityThreshold);
method public static <T> androidx.compose.animation.core.TransitionDefinition<T> transitionDefinition(kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.TransitionDefinition<T>,kotlin.Unit> init);
method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.TweenSpec<T> tween(optional int durationMillis, optional int delayMillis, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> easing);
}
+ public final class TransitionKt {
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.Bounds> animateBounds(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.Bounds>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.Bounds> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.Dp> animateDp(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.Dp>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.Dp> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<java.lang.Float> animateFloat(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>> transitionSpec, kotlin.jvm.functions.Function1<? super S,java.lang.Float> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<java.lang.Integer> animateInt(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Integer>> transitionSpec, kotlin.jvm.functions.Function1<? super S,java.lang.Integer> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.IntOffset> animateIntOffset(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.IntOffset> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.IntSize> animateIntSize(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.IntSize> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.geometry.Offset> animateOffset(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.geometry.Offset>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.geometry.Offset> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.Position> animatePosition(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.Position>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.Position> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.geometry.Rect> animateRect(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.geometry.Rect>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.geometry.Rect> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.geometry.Size> animateSize(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.geometry.Size>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.geometry.Size> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S, T, V extends androidx.compose.animation.core.AnimationVector> androidx.compose.runtime.State<T> animateValue(androidx.compose.animation.core.Transition<S>, androidx.compose.animation.core.TwoWayConverter<T,V> typeConverter, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<T>> transitionSpec, kotlin.jvm.functions.Function1<? super S,? extends T> targetValueByState);
+ method @androidx.compose.runtime.Composable public static <T> androidx.compose.animation.core.Transition<T> updateTransition(T? targetState, optional kotlin.jvm.functions.Function1<? super T,kotlin.Unit> onFinished);
+ }
+
public final class TransitionSpec<S> {
method public androidx.compose.animation.core.InterruptionHandling getInterruptionHandling();
method public S? getNextState();
@@ -561,6 +608,17 @@
property public abstract kotlin.jvm.functions.Function1<T,V> convertToVector;
}
+ public final class VectorConvertersKt {
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Rect,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.geometry.Rect.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Dp,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(androidx.compose.ui.unit.Dp.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Position,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.Position.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Size,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Size.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Bounds,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.unit.Bounds.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Offset.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntOffset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntOffset.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntSize,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntSize.Companion);
+ }
+
public interface VectorizedAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> {
method public long getDurationMillis(V start, V end, V startVelocity);
method public default V getEndVelocity(V start, V end, V startVelocity);
@@ -571,7 +629,7 @@
public final class VectorizedAnimationSpecKt {
}
- public interface VectorizedDurationBasedAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> extends androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+ public interface VectorizedDurationBasedAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> extends androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
method public int getDelayMillis();
method public int getDurationMillis();
method public default long getDurationMillis(V start, V end, V startVelocity);
@@ -579,13 +637,20 @@
property public abstract int durationMillis;
}
- public final class VectorizedFloatAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+ public interface VectorizedFiniteAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> extends androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+ }
+
+ public final class VectorizedFloatAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
ctor public VectorizedFloatAnimationSpec(androidx.compose.animation.core.FloatAnimationSpec anim);
method public long getDurationMillis(V start, V end, V startVelocity);
method public V getValue(long playTime, V start, V end, V startVelocity);
method public V getVelocity(long playTime, V start, V end, V startVelocity);
}
+ public final class VectorizedInfiniteRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+ ctor public VectorizedInfiniteRepeatableSpec(androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> animation, androidx.compose.animation.core.RepeatMode repeatMode);
+ }
+
public final class VectorizedKeyframesSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> {
ctor public VectorizedKeyframesSpec(java.util.Map<java.lang.Integer,? extends kotlin.Pair<? extends V,? extends kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float>>> keyframes, int durationMillis, int delayMillis);
method public int getDelayMillis();
@@ -596,7 +661,7 @@
property public int durationMillis;
}
- public final class VectorizedRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+ public final class VectorizedRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
ctor public VectorizedRepeatableSpec(int iterations, androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> animation, androidx.compose.animation.core.RepeatMode repeatMode);
method public long getDurationMillis(V start, V end, V startVelocity);
method public V getValue(long playTime, V start, V end, V startVelocity);
@@ -614,7 +679,7 @@
property public int durationMillis;
}
- public final class VectorizedSpringSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+ public final class VectorizedSpringSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
ctor public VectorizedSpringSpec(float dampingRatio, float stiffness, V? visibilityThreshold);
method public float getDampingRatio();
method public float getStiffness();
@@ -635,5 +700,17 @@
property public final kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> easing;
}
+ public final class VisibilityThresholdsKt {
+ method public static long getVisibilityThreshold(androidx.compose.ui.unit.IntOffset.Companion);
+ method public static long getVisibilityThreshold(androidx.compose.ui.geometry.Offset.Companion);
+ method public static int getVisibilityThreshold(kotlin.jvm.internal.IntCompanionObject);
+ method public static float getVisibilityThreshold(androidx.compose.ui.unit.Dp.Companion);
+ method public static long getVisibilityThreshold(androidx.compose.ui.unit.Position.Companion);
+ method public static long getVisibilityThreshold(androidx.compose.ui.geometry.Size.Companion);
+ method public static long getVisibilityThreshold(androidx.compose.ui.unit.IntSize.Companion);
+ method public static androidx.compose.ui.geometry.Rect getVisibilityThreshold(androidx.compose.ui.geometry.Rect.Companion);
+ method public static androidx.compose.ui.unit.Bounds getVisibilityThreshold(androidx.compose.ui.unit.Bounds.Companion);
+ }
+
}
diff --git a/compose/animation/animation-core/api/restricted_current.txt b/compose/animation/animation-core/api/restricted_current.txt
index 2603144..1855bc4 100644
--- a/compose/animation/animation-core/api/restricted_current.txt
+++ b/compose/animation/animation-core/api/restricted_current.txt
@@ -55,7 +55,7 @@
public final class AnimationConstants {
field public static final int DefaultDurationMillis = 300; // 0x12c
field public static final androidx.compose.animation.core.AnimationConstants INSTANCE;
- field public static final int Infinite = 2147483647; // 0x7fffffff
+ field @Deprecated public static final int Infinite = 2147483647; // 0x7fffffff
}
public enum AnimationEndReason {
@@ -115,6 +115,7 @@
method public static androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> AnimationState-SZyJPdc(float initialValue, optional float initialVelocity, optional long lastFrameTime, optional long endTime, optional boolean isRunning);
method public static androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> copy-mN48zRs(androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D>, optional float value, optional float velocity, optional long lastFrameTime, optional long endTime, optional boolean isRunning);
method public static <T, V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.AnimationState<T,V> copy-upwry94(androidx.compose.animation.core.AnimationState<T,V>, optional T? value, optional V? velocityVector, optional long lastFrameTime, optional long endTime, optional boolean isRunning);
+ method public static <T, V extends androidx.compose.animation.core.AnimationVector> V createZeroVectorFrom(androidx.compose.animation.core.TwoWayConverter<T,V>, T? value);
method public static float getVelocity(androidx.compose.animation.core.AnimationState<java.lang.Float,androidx.compose.animation.core.AnimationVector1D>);
method public static float getVelocity(androidx.compose.animation.core.AnimationScope<java.lang.Float,androidx.compose.animation.core.AnimationVector1D>);
method public static boolean isFinished(androidx.compose.animation.core.AnimationState<?,?>);
@@ -235,7 +236,7 @@
method public void dispatchTime$metalava_module(long frameTimeMillis);
}
- public interface DurationBasedAnimationSpec<T> extends androidx.compose.animation.core.AnimationSpec<T> {
+ public interface DurationBasedAnimationSpec<T> extends androidx.compose.animation.core.FiniteAnimationSpec<T> {
method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
}
@@ -257,6 +258,10 @@
property public float absVelocityThreshold;
}
+ public interface FiniteAnimationSpec<T> extends androidx.compose.animation.core.AnimationSpec<T> {
+ method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
+ }
+
public interface FloatAnimationSpec extends androidx.compose.animation.core.AnimationSpec<java.lang.Float> {
method public long getDurationMillis(float start, float end, float startVelocity);
method public default float getEndVelocity(float start, float end, float startVelocity);
@@ -309,6 +314,15 @@
property public final int duration;
}
+ public final class InfiniteRepeatableSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+ ctor public InfiniteRepeatableSpec(androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, androidx.compose.animation.core.RepeatMode repeatMode);
+ method public androidx.compose.animation.core.DurationBasedAnimationSpec<T> getAnimation();
+ method public androidx.compose.animation.core.RepeatMode getRepeatMode();
+ method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
+ property public final androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation;
+ property public final androidx.compose.animation.core.RepeatMode repeatMode;
+ }
+
public final class IntPropKey implements androidx.compose.animation.core.PropKey<java.lang.Integer,androidx.compose.animation.core.AnimationVector1D> {
ctor public IntPropKey(String label);
ctor public IntPropKey();
@@ -394,8 +408,6 @@
public final class PropKeyKt {
method public static <T, V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.TwoWayConverter<T,V> TwoWayConverter(kotlin.jvm.functions.Function1<? super T,? extends V> convertToVector, kotlin.jvm.functions.Function1<? super V,? extends T> convertFromVector);
- method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> getFloatToVectorConverter();
- method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<java.lang.Integer,androidx.compose.animation.core.AnimationVector1D> getIntToVectorConverter();
method public static androidx.compose.animation.core.TwoWayConverter<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(kotlin.jvm.internal.FloatCompanionObject);
method public static androidx.compose.animation.core.TwoWayConverter<java.lang.Integer,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(kotlin.jvm.internal.IntCompanionObject);
}
@@ -405,18 +417,18 @@
enum_constant public static final androidx.compose.animation.core.RepeatMode Reverse;
}
- @androidx.compose.runtime.Immutable public final class RepeatableSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+ @androidx.compose.runtime.Immutable public final class RepeatableSpec<T> implements androidx.compose.animation.core.FiniteAnimationSpec<T> {
ctor public RepeatableSpec(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, androidx.compose.animation.core.RepeatMode repeatMode);
method public androidx.compose.animation.core.DurationBasedAnimationSpec<T> getAnimation();
method public int getIterations();
method public androidx.compose.animation.core.RepeatMode getRepeatMode();
- method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
+ method public <V extends androidx.compose.animation.core.AnimationVector> androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> vectorize(androidx.compose.animation.core.TwoWayConverter<T,V> converter);
property public final androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation;
property public final int iterations;
property public final androidx.compose.animation.core.RepeatMode repeatMode;
}
- @androidx.compose.runtime.Immutable public final class SnapSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+ @androidx.compose.runtime.Immutable public final class SnapSpec<T> implements androidx.compose.animation.core.DurationBasedAnimationSpec<T> {
ctor public SnapSpec(int delay);
ctor public SnapSpec();
method public int getDelay();
@@ -443,7 +455,7 @@
public final class SpringSimulationKt {
}
- @androidx.compose.runtime.Immutable public final class SpringSpec<T> implements androidx.compose.animation.core.AnimationSpec<T> {
+ @androidx.compose.runtime.Immutable public final class SpringSpec<T> implements androidx.compose.animation.core.FiniteAnimationSpec<T> {
ctor public SpringSpec(float dampingRatio, float stiffness, T? visibilityThreshold);
ctor public SpringSpec();
method public float getDampingRatio();
@@ -489,6 +501,43 @@
public final class ToolingGlueKt {
}
+ public final class Transition<S> {
+ method @kotlin.PublishedApi internal boolean addAnimation(androidx.compose.animation.core.Transition<S>.TransitionAnimationState<?,?> animation);
+ method public S! getCurrentState();
+ method public S! getTargetState();
+ method public androidx.compose.animation.core.Transition.States<S> getTransitionStates();
+ method public boolean isRunning();
+ method @kotlin.PublishedApi internal void removeAnimation(androidx.compose.animation.core.Transition<S>.TransitionAnimationState<?,?> animation);
+ property public final S! currentState;
+ property public final boolean isRunning;
+ property public final S! targetState;
+ property public final androidx.compose.animation.core.Transition.States<S> transitionStates;
+ }
+
+ public static final class Transition.States<S> {
+ ctor public Transition.States(S? initialState, S? targetState);
+ method public S! getInitialState();
+ method public S! getTargetState();
+ property public final S! initialState;
+ property public final S! targetState;
+ }
+
+ @kotlin.PublishedApi internal final class Transition.TransitionAnimationState<T, V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.runtime.State<T> {
+ ctor @kotlin.PublishedApi internal Transition.TransitionAnimationState(T? initialValue, V initialVelocityVector, androidx.compose.animation.core.TwoWayConverter<T,V> typeConverter);
+ method public T! getTargetValue();
+ method public androidx.compose.animation.core.TwoWayConverter<T,V> getTypeConverter();
+ method public T! getValue();
+ method public V getVelocityVector();
+ method public boolean isFinished();
+ method @kotlin.PublishedApi internal void updateTargetValue(T? targetValue);
+ property public final boolean isFinished;
+ property public final T! targetValue;
+ property public final androidx.compose.animation.core.TwoWayConverter<T,V> typeConverter;
+ property public T! value;
+ property public final V velocityVector;
+ field @kotlin.PublishedApi internal androidx.compose.animation.core.FiniteAnimationSpec<T> animationSpec;
+ }
+
public final class TransitionAnimation<T> implements androidx.compose.animation.core.TransitionState {
ctor public TransitionAnimation(androidx.compose.animation.core.TransitionDefinition<T> def, androidx.compose.animation.core.AnimationClockObservable clock, T? initState, String? label);
method public operator <T, V extends androidx.compose.animation.core.AnimationVector> T! get(androidx.compose.animation.core.PropKey<T,V> propKey);
@@ -520,14 +569,30 @@
public final class TransitionDefinitionKt {
method public static <T> androidx.compose.animation.core.TransitionAnimation<T> createAnimation(androidx.compose.animation.core.TransitionDefinition<T>, androidx.compose.animation.core.AnimationClockObservable clock, optional T? initState);
+ method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.AnimationSpec<T> infiniteRepeatable(androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.KeyframesSpec<T> keyframes(kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.KeyframesSpec.KeyframesSpecConfig<T>,kotlin.Unit> init);
- method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.AnimationSpec<T> repeatable(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
- method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.AnimationSpec<T> snap(optional int delayMillis);
+ method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.RepeatableSpec<T> repeatable(int iterations, androidx.compose.animation.core.DurationBasedAnimationSpec<T> animation, optional androidx.compose.animation.core.RepeatMode repeatMode);
+ method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.SnapSpec<T> snap(optional int delayMillis);
method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.SpringSpec<T> spring(optional float dampingRatio, optional float stiffness, optional T? visibilityThreshold);
method public static <T> androidx.compose.animation.core.TransitionDefinition<T> transitionDefinition(kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.TransitionDefinition<T>,kotlin.Unit> init);
method @androidx.compose.runtime.Stable public static <T> androidx.compose.animation.core.TweenSpec<T> tween(optional int durationMillis, optional int delayMillis, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> easing);
}
+ public final class TransitionKt {
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.Bounds> animateBounds(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.Bounds>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.Bounds> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.Dp> animateDp(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.Dp>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.Dp> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<java.lang.Float> animateFloat(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float>> transitionSpec, kotlin.jvm.functions.Function1<? super S,java.lang.Float> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<java.lang.Integer> animateInt(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Integer>> transitionSpec, kotlin.jvm.functions.Function1<? super S,java.lang.Integer> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.IntOffset> animateIntOffset(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.IntOffset> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.IntSize> animateIntSize(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntSize>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.IntSize> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.geometry.Offset> animateOffset(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.geometry.Offset>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.geometry.Offset> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.unit.Position> animatePosition(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.Position>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.unit.Position> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.geometry.Rect> animateRect(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.geometry.Rect>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.geometry.Rect> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.geometry.Size> animateSize(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.geometry.Size>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.geometry.Size> targetValueByState);
+ method @androidx.compose.runtime.Composable public static inline <S, T, V extends androidx.compose.animation.core.AnimationVector> androidx.compose.runtime.State<T> animateValue(androidx.compose.animation.core.Transition<S>, androidx.compose.animation.core.TwoWayConverter<T,V> typeConverter, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<T>> transitionSpec, kotlin.jvm.functions.Function1<? super S,? extends T> targetValueByState);
+ method @androidx.compose.runtime.Composable public static <T> androidx.compose.animation.core.Transition<T> updateTransition(T? targetState, optional kotlin.jvm.functions.Function1<? super T,kotlin.Unit> onFinished);
+ }
+
public final class TransitionSpec<S> {
method public androidx.compose.animation.core.InterruptionHandling getInterruptionHandling();
method public S? getNextState();
@@ -561,6 +626,17 @@
property public abstract kotlin.jvm.functions.Function1<T,V> convertToVector;
}
+ public final class VectorConvertersKt {
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Rect,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.geometry.Rect.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Dp,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(androidx.compose.ui.unit.Dp.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Position,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.Position.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Size,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Size.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Bounds,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.unit.Bounds.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Offset.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntOffset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntOffset.Companion);
+ method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntSize,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntSize.Companion);
+ }
+
public interface VectorizedAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> {
method public long getDurationMillis(V start, V end, V startVelocity);
method public default V getEndVelocity(V start, V end, V startVelocity);
@@ -571,7 +647,7 @@
public final class VectorizedAnimationSpecKt {
}
- public interface VectorizedDurationBasedAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> extends androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+ public interface VectorizedDurationBasedAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> extends androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
method public int getDelayMillis();
method public int getDurationMillis();
method public default long getDurationMillis(V start, V end, V startVelocity);
@@ -579,13 +655,20 @@
property public abstract int durationMillis;
}
- public final class VectorizedFloatAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+ public interface VectorizedFiniteAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> extends androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+ }
+
+ public final class VectorizedFloatAnimationSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
ctor public VectorizedFloatAnimationSpec(androidx.compose.animation.core.FloatAnimationSpec anim);
method public long getDurationMillis(V start, V end, V startVelocity);
method public V getValue(long playTime, V start, V end, V startVelocity);
method public V getVelocity(long playTime, V start, V end, V startVelocity);
}
+ public final class VectorizedInfiniteRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+ ctor public VectorizedInfiniteRepeatableSpec(androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> animation, androidx.compose.animation.core.RepeatMode repeatMode);
+ }
+
public final class VectorizedKeyframesSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> {
ctor public VectorizedKeyframesSpec(java.util.Map<java.lang.Integer,? extends kotlin.Pair<? extends V,? extends kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float>>> keyframes, int durationMillis, int delayMillis);
method public int getDelayMillis();
@@ -596,7 +679,7 @@
property public int durationMillis;
}
- public final class VectorizedRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+ public final class VectorizedRepeatableSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
ctor public VectorizedRepeatableSpec(int iterations, androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec<V> animation, androidx.compose.animation.core.RepeatMode repeatMode);
method public long getDurationMillis(V start, V end, V startVelocity);
method public V getValue(long playTime, V start, V end, V startVelocity);
@@ -614,7 +697,7 @@
property public int durationMillis;
}
- public final class VectorizedSpringSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedAnimationSpec<V> {
+ public final class VectorizedSpringSpec<V extends androidx.compose.animation.core.AnimationVector> implements androidx.compose.animation.core.VectorizedFiniteAnimationSpec<V> {
ctor public VectorizedSpringSpec(float dampingRatio, float stiffness, V? visibilityThreshold);
method public float getDampingRatio();
method public float getStiffness();
@@ -635,5 +718,17 @@
property public final kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> easing;
}
+ public final class VisibilityThresholdsKt {
+ method public static long getVisibilityThreshold(androidx.compose.ui.unit.IntOffset.Companion);
+ method public static long getVisibilityThreshold(androidx.compose.ui.geometry.Offset.Companion);
+ method public static int getVisibilityThreshold(kotlin.jvm.internal.IntCompanionObject);
+ method public static float getVisibilityThreshold(androidx.compose.ui.unit.Dp.Companion);
+ method public static long getVisibilityThreshold(androidx.compose.ui.unit.Position.Companion);
+ method public static long getVisibilityThreshold(androidx.compose.ui.geometry.Size.Companion);
+ method public static long getVisibilityThreshold(androidx.compose.ui.unit.IntSize.Companion);
+ method public static androidx.compose.ui.geometry.Rect getVisibilityThreshold(androidx.compose.ui.geometry.Rect.Companion);
+ method public static androidx.compose.ui.unit.Bounds getVisibilityThreshold(androidx.compose.ui.unit.Bounds.Companion);
+ }
+
}
diff --git a/compose/animation/animation-core/build.gradle b/compose/animation/animation-core/build.gradle
index 16c9b69..781604b3 100644
--- a/compose/animation/animation-core/build.gradle
+++ b/compose/animation/animation-core/build.gradle
@@ -17,7 +17,6 @@
import androidx.build.AndroidXUiPlugin
import androidx.build.LibraryGroups
-import androidx.build.LibraryVersions
import androidx.build.Publish
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@@ -56,7 +55,10 @@
androidTestImplementation(ANDROIDX_TEST_RULES)
androidTestImplementation(ANDROIDX_TEST_RUNNER)
+ androidTestImplementation(ANDROIDX_TEST_CORE)
androidTestImplementation(JUNIT)
+ androidTestImplementation(project(":compose:animation:animation"))
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
}
}
@@ -96,7 +98,10 @@
androidAndroidTest.dependencies {
implementation(ANDROIDX_TEST_RULES)
implementation(ANDROIDX_TEST_RUNNER)
+ implementation(ANDROIDX_TEST_CORE)
implementation(JUNIT)
+ implementation project(":compose:animation:animation")
+ implementation project(":compose:ui:ui-test-junit4")
}
}
}
@@ -119,3 +124,14 @@
]
}
}
+
+android {
+ defaultConfig {
+ minSdkVersion 21
+ }
+ tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ useIR = true
+ }
+ }
+}
diff --git a/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/SuspendAnimationSamples.kt b/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/SuspendAnimationSamples.kt
index cc0dda0..43d0a9e 100644
--- a/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/SuspendAnimationSamples.kt
+++ b/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/SuspendAnimationSamples.kt
@@ -17,14 +17,13 @@
package androidx.compose.animation.core.samples
import androidx.annotation.Sampled
-import androidx.compose.animation.core.AnimationConstants
import androidx.compose.animation.core.AnimationState
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animate
import androidx.compose.animation.core.animateTo
+import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.isFinished
-import androidx.compose.animation.core.repeatable
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
@@ -82,8 +81,7 @@
animate(
initialValue = 1f,
targetValue = 0f,
- animationSpec = repeatable(
- iterations = AnimationConstants.Infinite,
+ animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
)
diff --git a/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/TransitionSamples.kt b/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/TransitionSamples.kt
new file mode 100644
index 0000000..61845d2
--- /dev/null
+++ b/compose/animation/animation-core/samples/src/main/java/androidx/compose/animation/core/samples/TransitionSamples.kt
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.core.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.keyframes
+import androidx.compose.animation.core.snap
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.Button
+import androidx.compose.material.MaterialTheme
+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.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.gesture.pressIndicatorGestureFilter
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.unit.dp
+
+@Sampled
+@Composable
+fun GestureAnimationSample() {
+ // enum class ComponentState { Pressed, Released }
+ var useRed by remember { mutableStateOf(false) }
+ var toState by remember { mutableStateOf(ComponentState.Released) }
+ val modifier = Modifier.pressIndicatorGestureFilter(
+ onStart = { toState = ComponentState.Pressed },
+ onStop = { toState = ComponentState.Released },
+ onCancel = { toState = ComponentState.Released }
+ )
+
+ // Defines a transition of `ComponentState`, and updates the transition when the provided
+ // [targetState] changes. The transition will run all of the child animations towards the new
+ // [targetState] in response to the [targetState] change.
+ val transition: Transition<ComponentState> = updateTransition(targetState = toState)
+ // Defines a float animation as a child animation the transition. The current animation value
+ // can be read from the returned State<Float>.
+ val scale: Float by transition.animateFloat(
+ // Defines a transition spec that uses the same low-stiffness spring for *all*
+ // transitions of this float, no matter what the target is.
+ transitionSpec = { spring(stiffness = 50f) }
+ ) { state ->
+ // This code block declares a mapping from state to value.
+ if (state == ComponentState.Pressed) 3f else 1f
+ }
+
+ // Defines a color animation as a child animation of the transition.
+ val color: Color by transition.animateColor(
+ transitionSpec = { transitionStates ->
+ if (transitionStates.initialState == ComponentState.Pressed &&
+ transitionStates.targetState == ComponentState.Released
+ ) {
+ // Uses spring for the transition going from pressed to released
+ spring(stiffness = 50f)
+ } else {
+ // Uses tween for all the other transitions. (In this case there is
+ // only one other transition. i.e. released -> pressed.)
+ tween(durationMillis = 500)
+ }
+ }
+ ) { state ->
+ when (state) {
+ // Similar to the float animation, we need to declare the target values
+ // for each state. In this code block we can access theme colors.
+ ComponentState.Pressed -> MaterialTheme.colors.primary
+ // We can also have the target value depend on other mutableStates,
+ // such as `useRed` here. Whenever the target value changes, transition
+ // will automatically animate to the new value even if it has already
+ // arrived at its target state.
+ ComponentState.Released -> if (useRed) Color.Red else MaterialTheme.colors.secondary
+ }
+ }
+ Column {
+ Button(
+ modifier = Modifier.padding(10.dp).align(Alignment.CenterHorizontally),
+ onClick = { useRed = !useRed }
+ ) {
+ Text("Change Color")
+ }
+ Box(
+ modifier.fillMaxSize().wrapContentSize(Alignment.Center)
+ .size((100 * scale).dp).background(color)
+ )
+ }
+}
+
+private enum class ComponentState { Pressed, Released }
+private enum class ButtonStatus { Initial, Pressed, Released }
+
+@Sampled
+@Composable
+fun AnimateFloatSample() {
+ // enum class ButtonStatus {Initial, Pressed, Released}
+ @Composable
+ fun AnimateAlphaAndScale(
+ modifier: Modifier,
+ transition: Transition<ButtonStatus>
+ ) {
+ // Defines a float animation as a child animation of transition. This allows the
+ // transition to manage the states of this animation. The returned State<Float> from the
+ // [animateFloat] function is used here as a property delegate.
+ // This float animation will use the default [spring] for all transition destinations, as
+ // specified by the default `transitionSpec`.
+ val scale: Float by transition.animateFloat { state ->
+ if (state == ButtonStatus.Pressed) 1.2f else 1f
+ }
+
+ // Alternatively, we can specify different animation specs based on the initial state and
+ // target state of the a transition run using `transitionSpec`.
+ val alpha: Float by transition.animateFloat(
+ transitionSpec = {
+ if (it.initialState == ButtonStatus.Initial &&
+ it.targetState == ButtonStatus.Pressed
+ ) {
+ keyframes {
+ durationMillis = 225
+ 0f at 0 // optional
+ 0.3f at 75
+ 0.2f at 225 // optional
+ }
+ } else if (it.initialState == ButtonStatus.Pressed &&
+ it.targetState == ButtonStatus.Released
+ ) {
+ tween(durationMillis = 220)
+ } else {
+ snap()
+ }
+ }
+ ) { state ->
+ // Same target value for Initial and Released states
+ if (state == ButtonStatus.Pressed) 0.2f else 0f
+ }
+
+ Box(modifier.graphicsLayer(alpha = alpha, scaleX = scale)) {
+ // content goes here
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/animation/animation-core/src/androidAndroidTest/AndroidManifest.xml b/compose/animation/animation-core/src/androidAndroidTest/AndroidManifest.xml
new file mode 100644
index 0000000..61f91c5
--- /dev/null
+++ b/compose/animation/animation-core/src/androidAndroidTest/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest package="androidx.compose.animation.core"/>
diff --git a/compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/TransitionTest.kt b/compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/TransitionTest.kt
new file mode 100644
index 0000000..6d4d2ac
--- /dev/null
+++ b/compose/animation/animation-core/src/androidAndroidTest/kotlin/androidx/compose/animation/core/TransitionTest.kt
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.core
+
+import androidx.compose.animation.VectorConverter
+import androidx.compose.animation.animateColor
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import junit.framework.TestCase.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+class TransitionTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private enum class AnimStates {
+ From,
+ To
+ }
+
+ @Test
+ fun transitionTest() {
+ val target = mutableStateOf(AnimStates.From)
+ val floatAnim1 = TargetBasedAnimation(
+ spring(dampingRatio = Spring.DampingRatioHighBouncy),
+ 0f,
+ 1f,
+ Float.VectorConverter
+ )
+ val floatAnim2 = TargetBasedAnimation(
+ spring(dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessLow),
+ 1f,
+ 0f,
+ Float.VectorConverter
+ )
+
+ val colorAnim1 = TargetBasedAnimation(
+ tween(1000),
+ Color.Red,
+ Color.Green,
+ Color.VectorConverter(Color.Red.colorSpace)
+ )
+ val colorAnim2 = TargetBasedAnimation(
+ tween(1000),
+ Color.Green,
+ Color.Red,
+ Color.VectorConverter(Color.Red.colorSpace)
+ )
+
+ // Animate from 0f to 0f for 1000ms
+ val keyframes1 = keyframes<Float> {
+ durationMillis = 1000
+ 0f at 0
+ 200f at 400
+ 1000f at 1000
+ }
+
+ val keyframes2 = keyframes<Float> {
+ durationMillis = 800
+ 0f at 0
+ -500f at 400
+ -1000f at 800
+ }
+
+ val keyframesAnim1 = TargetBasedAnimation(
+ keyframes1,
+ 0f,
+ 0f,
+ Float.VectorConverter
+ )
+ val keyframesAnim2 = TargetBasedAnimation(
+ keyframes2,
+ 0f,
+ 0f,
+ Float.VectorConverter
+ )
+ val animFloat = mutableStateOf(-1f)
+ val animColor = mutableStateOf(Color.Gray)
+ val animFloatWithKeyframes = mutableStateOf(-1f)
+ rule.setContent {
+ val transition = updateTransition(target.value)
+ animFloat.value = transition.animateFloat(
+ {
+ if (it.initialState == AnimStates.From && it.targetState == AnimStates.To) {
+ spring(dampingRatio = Spring.DampingRatioHighBouncy)
+ } else {
+ spring(
+ dampingRatio = Spring.DampingRatioLowBouncy,
+ stiffness = Spring.StiffnessLow
+ )
+ }
+ }
+ ) {
+ when (it) {
+ AnimStates.From -> 0f
+ AnimStates.To -> 1f
+ }
+ }.value
+
+ animColor.value = transition.animateColor(
+ { tween(durationMillis = 1000) }
+ ) {
+ when (it) {
+ AnimStates.From -> Color.Red
+ AnimStates.To -> Color.Green
+ }
+ }.value
+
+ animFloatWithKeyframes.value = transition.animateFloat(
+ transitionSpec = {
+ if (it.initialState == AnimStates.From && it.targetState == AnimStates.To) {
+ keyframes1
+ } else {
+ keyframes2
+ }
+ }
+ ) {
+ // Same values for all states, but different transitions from state to state.
+ 0f
+ }.value
+
+ if (transition.isRunning) {
+ if (transition.targetState == AnimStates.To) {
+ assertEquals(
+ floatAnim1.getValue(transition.playTimeNanos / 1_000_000L),
+ animFloat.value, 0.00001f
+ )
+ assertEquals(
+ colorAnim1.getValue(transition.playTimeNanos / 1_000_000L),
+ animColor.value
+ )
+ assertEquals(
+ keyframesAnim1.getValue(transition.playTimeNanos / 1_000_000L),
+ animFloatWithKeyframes.value, 0.00001f
+ )
+
+ assertEquals(AnimStates.To, transition.transitionStates.targetState)
+ assertEquals(AnimStates.From, transition.transitionStates.initialState)
+ } else {
+ assertEquals(
+ floatAnim2.getValue(transition.playTimeNanos / 1_000_000L),
+ animFloat.value, 0.00001f
+ )
+ assertEquals(
+ colorAnim2.getValue(transition.playTimeNanos / 1_000_000L),
+ animColor.value
+ )
+ assertEquals(
+ keyframesAnim2.getValue(transition.playTimeNanos / 1_000_000L),
+ animFloatWithKeyframes.value, 0.00001f
+ )
+ assertEquals(AnimStates.From, transition.transitionStates.targetState)
+ assertEquals(AnimStates.To, transition.transitionStates.initialState)
+ }
+ }
+ }
+
+ assertEquals(0f, animFloat.value)
+ assertEquals(Color.Red, animColor.value)
+ rule.runOnIdle {
+ target.value = AnimStates.To
+ }
+ rule.waitForIdle()
+
+ assertEquals(1f, animFloat.value)
+ assertEquals(Color.Green, animColor.value)
+
+ // Animate back to the `from` state
+ rule.runOnIdle {
+ target.value = AnimStates.From
+ }
+ rule.waitForIdle()
+
+ assertEquals(0f, animFloat.value)
+ assertEquals(Color.Red, animColor.value)
+ }
+}
\ No newline at end of file
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimatedValue.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimatedValue.kt
index a457db21..e04c380 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimatedValue.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimatedValue.kt
@@ -185,8 +185,8 @@
}
lastFrameTime = timeMillis
- value = anim.getValue(playtime)
velocityVector = anim.getVelocityVector(playtime)
+ value = anim.getValue(playtime)
checkFinished(playtime)
}
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationSpec.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationSpec.kt
index 8c71f54..7f7b55a 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationSpec.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationSpec.kt
@@ -28,8 +28,13 @@
const val DefaultDurationMillis: Int = 300
/**
- * Used as a iterations count for [VectorizedRepeatableSpec] to create an infinity repeating animation.
+ * Used as a iterations count for [VectorizedRepeatableSpec] to create an infinity repeating
+ * animation.
*/
+ @Deprecated(
+ "Using Infinite to specify repeatable animation iterations has been " +
+ "deprecated. Please use [InfiniteRepeatableSpec] or [infiniteRepeatable] instead."
+ )
const val Infinite: Int = Int.MAX_VALUE
}
@@ -65,6 +70,19 @@
}
/**
+ * [FiniteAnimationSpec] is the interface that all non-infinite [AnimationSpec]s implement,
+ * including: [TweenSpec], [SpringSpec], [KeyframesSpec], [RepeatableSpec], [SnapSpec], etc. By
+ * definition, [InfiniteRepeatableSpec] __does not__ implement this interface.
+ *
+ * @see [InfiniteRepeatableSpec]
+ */
+interface FiniteAnimationSpec<T> : AnimationSpec<T> {
+ override fun <V : AnimationVector> vectorize(
+ converter: TwoWayConverter<T, V>
+ ): VectorizedFiniteAnimationSpec<V>
+}
+
+/**
* Creates a TweenSpec configured with the given duration, delay, and easing curve.
*
* @param durationMillis duration of the [VectorizedTweenSpec] animation.
@@ -100,7 +118,7 @@
* [TweenSpec], and [SnapSpec]. These duration based specs can repeated when put into a
* [RepeatableSpec].
*/
-interface DurationBasedAnimationSpec<T> : AnimationSpec<T> {
+interface DurationBasedAnimationSpec<T> : FiniteAnimationSpec<T> {
override fun <V : AnimationVector> vectorize(converter: TwoWayConverter<T, V>):
VectorizedDurationBasedAnimationSpec<V>
}
@@ -114,12 +132,13 @@
* @param stiffness stiffness of the spring. [Spring.StiffnessMedium] by default.
* @param visibilityThreshold specifies the visibility threshold
*/
+// TODO: annotate damping/stiffness with FloatRange
@Immutable
class SpringSpec<T>(
val dampingRatio: Float = Spring.DampingRatioNoBouncy,
val stiffness: Float = Spring.StiffnessMedium,
val visibilityThreshold: T? = null
-) : AnimationSpec<T> {
+) : FiniteAnimationSpec<T> {
override fun <V : AnimationVector> vectorize(converter: TwoWayConverter<T, V>) =
VectorizedSpringSpec(dampingRatio, stiffness, converter.convert(visibilityThreshold))
@@ -146,14 +165,18 @@
}
/**
- * [RepeatableSpec] takes another [DurationBasedAnimationSpec] and plays it [iterations] times.
+ * [RepeatableSpec] takes another [DurationBasedAnimationSpec] and plays it [iterations] times. For
+ * creating infinitely repeating animation spec, consider using [InfiniteRepeatableSpec].
*
* __Note__: When repeating in the [RepeatMode.Reverse] mode, it's highly recommended to have an
- * __odd__ number of iterations, or [AnimationConstants.Infinite] iterations. Otherwise, the
- * animation may jump to the end value when it finishes the last iteration.
+ * __odd__ number of iterations. Otherwise, the animation may jump to the end value when it finishes
+ * the last iteration.
*
- * @param iterations the count of iterations. Should be at least 1. [AnimationConstants.Infinite]
- * can be used to have an infinity repeating animation.
+ * @see repeatable
+ * @see InfiniteRepeatableSpec
+ * @see infiniteRepeatable
+ *
+ * @param iterations the count of iterations. Should be at least 1.
* @param animation the [AnimationSpec] to be repeated
* @param repeatMode whether animation should repeat by starting from the beginning (i.e.
* [RepeatMode.Restart]) or from the end (i.e. [RepeatMode.Reverse])
@@ -163,10 +186,10 @@
val iterations: Int,
val animation: DurationBasedAnimationSpec<T>,
val repeatMode: RepeatMode = RepeatMode.Restart
-) : AnimationSpec<T> {
+) : FiniteAnimationSpec<T> {
override fun <V : AnimationVector> vectorize(
converter: TwoWayConverter<T, V>
- ): VectorizedAnimationSpec<V> {
+ ): VectorizedFiniteAnimationSpec<V> {
return VectorizedRepeatableSpec(iterations, animation.vectorize(converter), repeatMode)
}
@@ -185,6 +208,42 @@
}
/**
+ * [InfiniteRepeatableSpec] repeats the provided [animation] infinite amount of times. It will
+ * never naturally finish. This means the animation will only be stopped via some form of manual
+ * cancellation. When used with transition or other animation composables, the infinite animations
+ * will stop when the composable is removed from the compose tree.
+ *
+ * For non-infinite repeating animations, consider [RepeatableSpec].
+ *
+ * @param animation the [AnimationSpec] to be repeated
+ * @param repeatMode whether animation should repeat by starting from the beginning (i.e.
+ * [RepeatMode.Restart]) or from the end (i.e. [RepeatMode.Reverse])
+ * @see infiniteRepeatable
+ */
+// TODO: Consider supporting repeating spring specs
+class InfiniteRepeatableSpec<T>(
+ val animation: DurationBasedAnimationSpec<T>,
+ val repeatMode: RepeatMode = RepeatMode.Restart
+) : AnimationSpec<T> {
+ override fun <V : AnimationVector> vectorize(
+ converter: TwoWayConverter<T, V>
+ ): VectorizedAnimationSpec<V> {
+ return VectorizedInfiniteRepeatableSpec(animation.vectorize(converter), repeatMode)
+ }
+
+ override fun equals(other: Any?): Boolean =
+ if (other is RepeatableSpec<*>) {
+ other.animation == this.animation && other.repeatMode == this.repeatMode
+ } else {
+ false
+ }
+
+ override fun hashCode(): Int {
+ return animation.hashCode() * 31 + repeatMode.hashCode()
+ }
+}
+
+/**
* Repeat mode for [RepeatableSpec] and [VectorizedRepeatableSpec].
*/
enum class RepeatMode {
@@ -207,7 +266,7 @@
* starts. Defaults to 0.
*/
@Immutable
-class SnapSpec<T>(val delay: Int = 0) : AnimationSpec<T> {
+class SnapSpec<T>(val delay: Int = 0) : DurationBasedAnimationSpec<T> {
override fun <V : AnimationVector> vectorize(
converter: TwoWayConverter<T, V>
): VectorizedDurationBasedAnimationSpec<V> = VectorizedSnapSpec(delay)
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationState.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationState.kt
index be201bb..945e9d4 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationState.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationState.kt
@@ -54,7 +54,8 @@
/**
* Current velocity vector of the [AnimationState].
*/
- var velocityVector: V = initialVelocityVector ?: typeConverter.createZeroVector(initialValue)
+ var velocityVector: V =
+ initialVelocityVector ?: typeConverter.createZeroVectorFrom(initialValue)
internal set
/**
@@ -243,5 +244,11 @@
)
}
-private fun <T, V : AnimationVector> TwoWayConverter<T, V>.createZeroVector(value: T) =
+/**
+ * Creates an AnimationVector with all the values set to 0 using the provided [TwoWayConverter]
+ * and the [value].
+ *
+ * @return a new AnimationVector instance of type [V].
+ */
+fun <T, V : AnimationVector> TwoWayConverter<T, V>.createZeroVectorFrom(value: T) =
convertToVector(value).newInstance()
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/PropKey.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/PropKey.kt
index 7b55b2e..05991c4 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/PropKey.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/PropKey.kt
@@ -110,22 +110,6 @@
/**
* A [TwoWayConverter] that converts [Float] from and to [AnimationVector1D]
- * @see [Float.Companion.VectorConverter]
- */
-@Deprecated("", ReplaceWith("Float.VectorConverter"))
-val FloatToVectorConverter: TwoWayConverter<Float, AnimationVector1D> =
- Float.VectorConverter
-
-/**
- * A [TwoWayConverter] that converts [Int] from and to [AnimationVector1D]
- * @see [Int.Companion.VectorConverter]
- */
-@Deprecated("", ReplaceWith("Int.VectorConverter"))
-val IntToVectorConverter: TwoWayConverter<Int, AnimationVector1D> =
- Int.VectorConverter
-
-/**
- * A [TwoWayConverter] that converts [Float] from and to [AnimationVector1D]
*/
val Float.Companion.VectorConverter: TwoWayConverter<Float, AnimationVector1D>
get() = FloatToVector
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
new file mode 100644
index 0000000..c91c1dd
--- /dev/null
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
@@ -0,0 +1,624 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.core
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collection.mutableVectorOf
+import androidx.compose.runtime.dispatch.withFrameNanos
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.unit.Bounds
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.Position
+import androidx.compose.ui.unit.Uptime
+import androidx.compose.ui.util.annotation.VisibleForTesting
+
+/**
+ * This sets up a [Transition], and updates it with the target provided by [targetState]. When
+ * [targetState] changes, [Transition] will run all of its child animations towards their
+ * target values specified for the new [targetState]. Child animations can be dynamically added
+ * using [Transition.animateFloat], [animateColor][ androidx.compose.animation.animateColor],
+ * [Transition.animateValue], etc.
+ *
+ * When all the animations in the transition have finished running, the provided [onFinished] will
+ * be invoked.
+ *
+ * @sample androidx.compose.animation.core.samples.GestureAnimationSample
+ *
+ * @return a [Transition] object, to which animations can be added.
+ * @see Transition
+ * @see animateFloat
+ * @see animateValue
+ * @see androidx.compose.animation.animateColor
+ */
+@Composable
+fun <T> updateTransition(
+ targetState: T,
+ onFinished: (T) -> Unit = {}
+): Transition<T> {
+ val listener = rememberUpdatedState(onFinished)
+ val transition = remember { Transition(targetState, listener) }
+ // This is needed because child animations rely on this target state and the state pair to
+ // update their animation specs
+ transition.updateTarget(targetState)
+ SideEffect {
+ transition.animateTo(targetState)
+ }
+ if (transition.isRunning || transition.startRequested) {
+ LaunchedEffect(transition) {
+ while (true) {
+ withFrameNanos {
+ transition.onFrame(it)
+ }
+ }
+ }
+ }
+ return transition
+}
+
+/**
+ * [Transition] manages all the child animations on a state level. Child animations
+ * can be created in a declarative way using [animateFloat], [animateValue],
+ * [animateColor][androidx.compose.animation.animateColor] etc. When the [targetState] changes,
+ * [Transition] will automatically start or adjust course for all its child animations to animate
+ * to the new target values defined for each animation.
+ *
+ * After arriving at [targetState], [Transition] will be triggered to run if any child animation
+ * changes its target value (due to their dynamic target calculation logic, such as theme-dependent
+ * values).
+ *
+ * @sample androidx.compose.animation.core.samples.GestureAnimationSample
+ *
+ * @return a [Transition] object, to which animations can be added.
+ * @see updateTransition
+ * @see animateFloat
+ * @see animateValue
+ * @see androidx.compose.animation.animateColor
+ */
+// TODO: Support creating Transition outside of composition and support imperative use of Transition
+class Transition<S> internal constructor(
+ initialState: S,
+ private val onFinished: State<(S) -> Unit>
+) {
+ /**
+ * Current state of the transition. This will always be the initialState of the transition
+ * until the transition is finished. Once the transition is finished, [currentState] will be
+ * set to [targetState].
+ */
+ var currentState: S by mutableStateOf(initialState)
+ internal set
+
+ /**
+ * Target state of the transition. This will be read by all child animations to determine their
+ * most up-to-date target values.
+ */
+ var targetState: S by mutableStateOf(initialState)
+ internal set
+
+ /**
+ * [transitionStates] contains the initial state and the target state of the currently on-going
+ * transition.
+ */
+ var transitionStates: States<S> by mutableStateOf(States(initialState, initialState))
+ private set
+
+ /**
+ * Indicates whether there is any animation running in the transition.
+ */
+ val isRunning: Boolean
+ get() = startTime != Uptime.Unspecified
+
+ /**
+ * Play time in nano-seconds. [playTimeNanos] is always non-negative. It starts from 0L at the
+ * beginning of the transition and increment until all child animations have finished.
+ */
+ @VisibleForTesting
+ internal var playTimeNanos by mutableStateOf(0L)
+ internal var startRequested: Boolean by mutableStateOf(false)
+ private var startTime = Uptime.Unspecified
+ private val animations = mutableVectorOf<TransitionAnimationState<*, *>>()
+
+ // Target state that is currently being animated to
+ private var currentTargetState: S = initialState
+
+ internal fun onFrame(frameTimeNanos: Long) {
+ if (startTime == Uptime.Unspecified) {
+ startTime = Uptime(frameTimeNanos)
+ }
+ startRequested = false
+
+ // Update play time
+ playTimeNanos = frameTimeNanos - startTime.nanoseconds
+ var allFinished = true
+ // Pulse new playtime
+ animations.forEach {
+ if (!it.isFinished) {
+ it.onPlayTimeChanged(playTimeNanos)
+ }
+ // Check isFinished flag again after the animation pulse
+ if (!it.isFinished) {
+ allFinished = false
+ }
+ }
+ if (allFinished) {
+ startTime = Uptime.Unspecified
+ currentState = targetState
+ playTimeNanos = 0
+ onFinished.value(targetState)
+ }
+ }
+
+ @PublishedApi
+ internal fun addAnimation(animation: TransitionAnimationState<*, *>) =
+ animations.add(animation)
+
+ @PublishedApi
+ internal fun removeAnimation(animation: TransitionAnimationState<*, *>) {
+ animations.remove(animation)
+ }
+
+ // This target state should only be used to modify "mutableState"s, as it could potentially
+ // roll back. The
+ internal fun updateTarget(targetState: S) {
+ if (transitionStates.targetState != targetState) {
+ if (currentState == targetState) {
+ // Going backwards
+ transitionStates = States(this.targetState, targetState)
+ } else {
+ transitionStates = States(currentState, targetState)
+ }
+ }
+ this.targetState = targetState
+ }
+
+ internal fun animateTo(targetState: S) {
+ if (targetState != currentTargetState) {
+ if (isRunning) {
+ startTime = Uptime(startTime.nanoseconds + playTimeNanos)
+ playTimeNanos = 0
+ } else {
+ startRequested = true
+ }
+ currentTargetState = targetState
+ // If target state is changed, reset all the animations to be re-created in the
+ // next frame w/ their new target value. Child animations target values are updated in
+ // the side effect that may not have happened when this function in invoked.
+ animations.forEach { it.resetAnimation() }
+ }
+ }
+
+ // Called from children to start an animation
+ private fun requestStart() {
+ startRequested = true
+ }
+
+ // TODO: Consider making this public
+ @PublishedApi
+ internal inner class TransitionAnimationState<T, V : AnimationVector> @PublishedApi internal
+ constructor(
+ initialValue: T,
+ initialVelocityVector: V,
+ val typeConverter: TwoWayConverter<T, V>
+ ) : State<T> {
+
+ override var value by mutableStateOf(initialValue)
+ internal set
+
+ var targetValue: T = initialValue
+ internal set
+ var velocityVector: V = initialVelocityVector
+ internal set
+ var isFinished: Boolean by mutableStateOf(true)
+ private set
+ private var animation: Animation<T, V>? = null
+
+ @PublishedApi
+ internal var animationSpec: FiniteAnimationSpec<T> = spring()
+ private var offsetTimeNanos = 0L
+
+ internal fun onPlayTimeChanged(playTimeNanos: Long) {
+ val anim = animation ?: TargetBasedAnimation<T, V>(
+ animationSpec,
+ value,
+ targetValue,
+ typeConverter,
+ velocityVector
+ ).also { animation = it }
+ val playTimeMillis = (playTimeNanos - offsetTimeNanos) / 1_000_000L
+ value = anim.getValue(playTimeMillis)
+ velocityVector = anim.getVelocityVector(playTimeMillis)
+ if (anim.isFinished(playTimeMillis)) {
+ isFinished = true
+ offsetTimeNanos = 0
+ }
+ }
+
+ internal fun resetAnimation() {
+ animation = null
+ offsetTimeNanos = 0
+ isFinished = false
+ }
+
+ @PublishedApi
+ // This gets called from a side effect.
+ internal fun updateTargetValue(targetValue: T) {
+ if (this.targetValue != targetValue) {
+ this.targetValue = targetValue
+ isFinished = false
+ animation = null
+ offsetTimeNanos = playTimeNanos
+ requestStart()
+ }
+ }
+ }
+
+ /**
+ * [States] holds [initialState] and [targetState], which are the beginning and end of a
+ * transition. These states will be used to obtain the animation spec that will be used for this
+ * transition from the child animations.
+ */
+ class States<S>(val initialState: S, val targetState: S)
+}
+
+/**
+ * Creates an animation of type [T] as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition]. [typeConverter] will be used to convert
+ * between type [T] and [AnimationVector] so that the animation system knows how to animate it.
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ * @see updateTransition
+ * @see animateFloat
+ * @see androidx.compose.animation.animateColor
+ */
+@Composable
+inline fun <S, T, V : AnimationVector> Transition<S>.animateValue(
+ typeConverter: TwoWayConverter<T, V>,
+ noinline transitionSpec: @Composable (Transition.States<S>) -> FiniteAnimationSpec<T> =
+ { spring() },
+ targetValueByState: @Composable (state: S) -> T
+): State<T> {
+ val targetValue = targetValueByState(targetState)
+ val transitionAnimation = remember {
+ TransitionAnimationState(
+ targetValue,
+ typeConverter.createZeroVectorFrom(targetValue),
+ typeConverter
+ )
+ }
+ val spec = transitionSpec(transitionStates)
+
+ SideEffect {
+ transitionAnimation.animationSpec = spec
+ transitionAnimation.updateTargetValue(targetValue)
+ }
+
+ DisposableEffect(transitionAnimation) {
+ addAnimation(transitionAnimation)
+ onDispose {
+ removeAnimation(transitionAnimation)
+ }
+ }
+ return transitionAnimation
+}
+
+// TODO: Remove noinline when b/174814083 is fixed.
+/**
+ * Creates a Float animation as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @sample androidx.compose.animation.core.samples.AnimateFloatSample
+ *
+ * @return A [State] object, the value of which is updated by animation
+ * @see updateTransition
+ * @see animateValue
+ * @see androidx.compose.animation.animateColor
+ */
+@Composable
+inline fun <S> Transition<S>.animateFloat(
+ noinline transitionSpec:
+ @Composable (Transition.States<S>) -> FiniteAnimationSpec<Float> = { spring() },
+ targetValueByState: @Composable (state: S) -> Float
+): State<Float> =
+ animateValue(Float.VectorConverter, transitionSpec, targetValueByState)
+
+/**
+ * Creates a [Dp] animation as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ */
+@Composable
+inline fun <S> Transition<S>.animateDp(
+ noinline transitionSpec: @Composable (Transition.States<S>) -> FiniteAnimationSpec<Dp> = {
+ spring(visibilityThreshold = Dp.VisibilityThreshold)
+ },
+ targetValueByState: @Composable (state: S) -> Dp
+): State<Dp> =
+ animateValue(Dp.VectorConverter, transitionSpec, targetValueByState)
+
+/**
+ * Creates an [Offset] animation as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ */
+@Composable
+inline fun <S> Transition<S>.animateOffset(
+ noinline transitionSpec: @Composable (Transition.States<S>) -> FiniteAnimationSpec<Offset> = {
+ spring(visibilityThreshold = Offset.VisibilityThreshold)
+ },
+ targetValueByState: @Composable (state: S) -> Offset
+): State<Offset> =
+ animateValue(Offset.VectorConverter, transitionSpec, targetValueByState)
+
+/**
+ * Creates a [Position] animation as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ */
+@Composable
+inline fun <S> Transition<S>.animatePosition(
+ noinline transitionSpec: @Composable (Transition.States<S>) -> FiniteAnimationSpec<Position> = {
+ spring(visibilityThreshold = Position.VisibilityThreshold)
+ },
+ targetValueByState: @Composable (state: S) -> Position
+): State<Position> =
+ animateValue(Position.VectorConverter, transitionSpec, targetValueByState)
+
+/**
+ * Creates a [Size] animation as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ */
+@Composable
+inline fun <S> Transition<S>.animateSize(
+ noinline transitionSpec: @Composable (Transition.States<S>) -> FiniteAnimationSpec<Size> = {
+ spring(visibilityThreshold = Size.VisibilityThreshold)
+ },
+ targetValueByState: @Composable (state: S) -> Size
+): State<Size> =
+ animateValue(Size.VectorConverter, transitionSpec, targetValueByState)
+
+/**
+ * Creates a [IntOffset] animation as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ */
+@Composable
+inline fun <S> Transition<S>.animateIntOffset(
+ noinline transitionSpec: @Composable (Transition.States<S>) -> FiniteAnimationSpec<IntOffset> =
+ { spring(visibilityThreshold = IntOffset(1, 1)) },
+ targetValueByState: @Composable (state: S) -> IntOffset
+): State<IntOffset> =
+ animateValue(IntOffset.VectorConverter, transitionSpec, targetValueByState)
+
+/**
+ * Creates a [Int] animation as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ */
+@Composable
+inline fun <S> Transition<S>.animateInt(
+ noinline transitionSpec: @Composable (Transition.States<S>) -> FiniteAnimationSpec<Int> = {
+ spring(visibilityThreshold = 1)
+ },
+ targetValueByState: @Composable (state: S) -> Int
+): State<Int> =
+ animateValue(Int.VectorConverter, transitionSpec, targetValueByState)
+
+/**
+ * Creates a [IntSize] animation as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ */
+@Composable
+inline fun <S> Transition<S>.animateIntSize(
+ noinline transitionSpec: @Composable (Transition.States<S>) -> FiniteAnimationSpec<IntSize> = {
+ spring(visibilityThreshold = IntSize(1, 1))
+ },
+ targetValueByState: @Composable (state: S) -> IntSize
+): State<IntSize> =
+ animateValue(IntSize.VectorConverter, transitionSpec, targetValueByState)
+
+/**
+ * Creates a [Bounds] animation as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ */
+@Composable
+inline fun <S> Transition<S>.animateBounds(
+ noinline transitionSpec: @Composable (Transition.States<S>) -> FiniteAnimationSpec<Bounds> = {
+ spring(visibilityThreshold = Bounds.VisibilityThreshold)
+ },
+ targetValueByState: @Composable (state: S) -> Bounds
+): State<Bounds> =
+ animateValue(Bounds.VectorConverter, transitionSpec, targetValueByState)
+
+/**
+ * Creates a [Rect] animation as a part of the given [Transition]. This means the states
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ */
+@Composable
+inline fun <S> Transition<S>.animateRect(
+ noinline transitionSpec: @Composable (Transition.States<S>) -> FiniteAnimationSpec<Rect> =
+ { spring(visibilityThreshold = Rect.VisibilityThreshold) },
+ targetValueByState: @Composable (state: S) -> Rect
+): State<Rect> =
+ animateValue(Rect.VectorConverter, transitionSpec, targetValueByState)
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/TransitionDefinition.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/TransitionDefinition.kt
index 91b09ba..bc77654 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/TransitionDefinition.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/TransitionDefinition.kt
@@ -17,7 +17,6 @@
package androidx.compose.animation.core
import androidx.compose.animation.core.AnimationConstants.DefaultDurationMillis
-import androidx.compose.animation.core.AnimationConstants.Infinite
import androidx.compose.runtime.Stable
import androidx.compose.ui.util.fastFirstOrNull
@@ -149,11 +148,11 @@
* [TweenSpec], [KeyframesSpec]) the amount of iterations specified by [iterations].
*
* The iteration count describes the amount of times the animation will run.
- * 1 means no repeat. Use [Infinite] to create an infinity repeating animation.
+ * 1 means no repeat. Recommend [infiniteRepeatable] for creating an infinity repeating animation.
*
* __Note__: When repeating in the [RepeatMode.Reverse] mode, it's highly recommended to have an
- * __odd__ number of iterations, or [AnimationConstants.Infinite] iterations. Otherwise, the
- * animation may jump to the end value when it finishes the last iteration.
+ * __odd__ number of iterations. Otherwise, the animation may jump to the end value when it finishes
+ * the last iteration.
*
* @param iterations the total count of iterations, should be greater than 1 to repeat.
* @param animation animation that will be repeated
@@ -165,16 +164,33 @@
iterations: Int,
animation: DurationBasedAnimationSpec<T>,
repeatMode: RepeatMode = RepeatMode.Restart
-): AnimationSpec<T> =
+): RepeatableSpec<T> =
RepeatableSpec(iterations, animation, repeatMode)
/**
+ * Creates a [InfiniteRepeatableSpec] that plays a [DurationBasedAnimationSpec] (e.g.
+ * [TweenSpec], [KeyframesSpec]) infinite amount of iterations.
+ *
+ * For non-infinitely repeating animations, consider [repeatable].
+ *
+ * @param animation animation that will be repeated
+ * @param repeatMode whether animation should repeat by starting from the beginning (i.e.
+ * [RepeatMode.Restart]) or from the end (i.e. [RepeatMode.Reverse])
+ */
+@Stable
+fun <T> infiniteRepeatable(
+ animation: DurationBasedAnimationSpec<T>,
+ repeatMode: RepeatMode = RepeatMode.Restart
+): AnimationSpec<T> =
+ InfiniteRepeatableSpec(animation, repeatMode)
+
+/**
* Creates a Snap animation for immediately switching the animating value to the end value.
*
* @param delayMillis the number of milliseconds to wait before the animation runs. 0 by default.
*/
@Stable
-fun <T> snap(delayMillis: Int = 0): AnimationSpec<T> = SnapSpec(delayMillis)
+fun <T> snap(delayMillis: Int = 0) = SnapSpec<T>(delayMillis)
/**
* [TransitionDefinition] contains all the animation related configurations that will be used in
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorConverters.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorConverters.kt
new file mode 100644
index 0000000..585df63
--- /dev/null
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorConverters.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.core
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.unit.Bounds
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.Position
+import androidx.compose.ui.unit.dp
+import kotlin.math.roundToInt
+
+/**
+ * A type converter that converts a [Rect] to a [AnimationVector4D], and vice versa.
+ */
+val Rect.Companion.VectorConverter: TwoWayConverter<Rect, AnimationVector4D>
+ get() = RectToVector
+
+/**
+ * A type converter that converts a [Dp] to a [AnimationVector1D], and vice versa.
+ */
+val Dp.Companion.VectorConverter: TwoWayConverter<Dp, AnimationVector1D>
+ get() = DpToVector
+
+/**
+ * A type converter that converts a [Position] to a [AnimationVector2D], and vice versa.
+ */
+val Position.Companion.VectorConverter: TwoWayConverter<Position, AnimationVector2D>
+ get() = PositionToVector
+
+/**
+ * A type converter that converts a [Size] to a [AnimationVector2D], and vice versa.
+ */
+val Size.Companion.VectorConverter: TwoWayConverter<Size, AnimationVector2D>
+ get() = SizeToVector
+
+/**
+ * A type converter that converts a [Bounds] to a [AnimationVector4D], and vice versa.
+ */
+val Bounds.Companion.VectorConverter: TwoWayConverter<Bounds, AnimationVector4D>
+ get() = BoundsToVector
+
+/**
+ * A type converter that converts a [Offset] to a [AnimationVector2D], and vice versa.
+ */
+val Offset.Companion.VectorConverter: TwoWayConverter<Offset, AnimationVector2D>
+ get() = OffsetToVector
+
+/**
+ * A type converter that converts a [IntOffset] to a [AnimationVector2D], and vice versa.
+ */
+val IntOffset.Companion.VectorConverter: TwoWayConverter<IntOffset, AnimationVector2D>
+ get() = IntOffsetToVector
+
+/**
+ * A type converter that converts a [IntSize] to a [AnimationVector2D], and vice versa.
+ */
+val IntSize.Companion.VectorConverter: TwoWayConverter<IntSize, AnimationVector2D>
+ get() = IntSizeToVector
+
+/**
+ * A type converter that converts a [Dp] to a [AnimationVector1D], and vice versa.
+ */
+private val DpToVector: TwoWayConverter<Dp, AnimationVector1D> = TwoWayConverter(
+ convertToVector = { AnimationVector1D(it.value) },
+ convertFromVector = { Dp(it.value) }
+)
+
+/**
+ * A type converter that converts a [Position] to a [AnimationVector2D], and vice versa.
+ */
+private val PositionToVector: TwoWayConverter<Position, AnimationVector2D> =
+ TwoWayConverter(
+ convertToVector = { AnimationVector2D(it.x.value, it.y.value) },
+ convertFromVector = { Position(it.v1.dp, it.v2.dp) }
+ )
+
+/**
+ * A type converter that converts a [Size] to a [AnimationVector2D], and vice versa.
+ */
+private val SizeToVector: TwoWayConverter<Size, AnimationVector2D> =
+ TwoWayConverter(
+ convertToVector = { AnimationVector2D(it.width, it.height) },
+ convertFromVector = { Size(it.v1, it.v2) }
+ )
+
+/**
+ * A type converter that converts a [Bounds] to a [AnimationVector4D], and vice versa.
+ */
+private val BoundsToVector: TwoWayConverter<Bounds, AnimationVector4D> =
+ TwoWayConverter(
+ convertToVector = {
+ AnimationVector4D(it.left.value, it.top.value, it.right.value, it.bottom.value)
+ },
+ convertFromVector = { Bounds(it.v1.dp, it.v2.dp, it.v3.dp, it.v4.dp) }
+ )
+
+/**
+ * A type converter that converts a [Offset] to a [AnimationVector2D], and vice versa.
+ */
+private val OffsetToVector: TwoWayConverter<Offset, AnimationVector2D> =
+ TwoWayConverter(
+ convertToVector = { AnimationVector2D(it.x, it.y) },
+ convertFromVector = { Offset(it.v1, it.v2) }
+ )
+
+/**
+ * A type converter that converts a [IntOffset] to a [AnimationVector2D], and vice versa.
+ */
+private val IntOffsetToVector: TwoWayConverter<IntOffset, AnimationVector2D> =
+ TwoWayConverter(
+ convertToVector = { AnimationVector2D(it.x.toFloat(), it.y.toFloat()) },
+ convertFromVector = { IntOffset(it.v1.roundToInt(), it.v2.roundToInt()) }
+ )
+
+/**
+ * A type converter that converts a [IntSize] to a [AnimationVector2D], and vice versa.
+ */
+private val IntSizeToVector: TwoWayConverter<IntSize, AnimationVector2D> =
+ TwoWayConverter(
+ { AnimationVector2D(it.width.toFloat(), it.height.toFloat()) },
+ { IntSize(it.v1.roundToInt(), it.v2.roundToInt()) }
+ )
+
+/**
+ * A type converter that converts a [Rect] to a [AnimationVector4D], and vice versa.
+ */
+private val RectToVector: TwoWayConverter<Rect, AnimationVector4D> =
+ TwoWayConverter(
+ convertToVector = {
+ AnimationVector4D(it.left, it.top, it.right, it.bottom)
+ },
+ convertFromVector = {
+ Rect(it.v1, it.v2, it.v3, it.v4)
+ }
+ )
\ No newline at end of file
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
index fd8d44d..bd1e235 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VectorizedAnimationSpec.kt
@@ -17,7 +17,6 @@
package androidx.compose.animation.core
import androidx.compose.animation.core.AnimationConstants.DefaultDurationMillis
-import androidx.compose.animation.core.AnimationConstants.Infinite
import kotlin.math.min
/**
@@ -107,13 +106,23 @@
}
/**
+ * All the finite [VectorizedAnimationSpec]s implement this interface, including:
+ * [VectorizedKeyframesSpec], [VectorizedTweenSpec], [VectorizedRepeatableSpec],
+ * [VectorizedSnapSpec], [VectorizedSpringSpec], etc. The [VectorizedAnimationSpec] that does
+ * __not__ implement this is: [InfiniteRepeatableSpec].
+ */
+interface VectorizedFiniteAnimationSpec<V : AnimationVector> : VectorizedAnimationSpec<V>
+
+/**
* Base class for [VectorizedAnimationSpec]s that are based on a fixed [durationMillis].
*/
-interface VectorizedDurationBasedAnimationSpec<V : AnimationVector> : VectorizedAnimationSpec<V> {
+interface VectorizedDurationBasedAnimationSpec<V : AnimationVector> :
+ VectorizedFiniteAnimationSpec<V> {
/**
* duration is the amount of time while animation is not yet finished.
*/
val durationMillis: Int
+
/**
* delay defines the amount of time that animation can be delayed.
*/
@@ -265,19 +274,41 @@
get() = 0
}
+private const val InfiniteIterations: Int = Int.MAX_VALUE
+
/**
* This animation takes another [VectorizedDurationBasedAnimationSpec] and plays it
- * [iterations] times.
+ * __infinite__ times.
*
- * @param iterations the count of iterations. Should be at least 1. [Infinite] can
- * be used to have an infinity repeating animation.
* @param animation the [VectorizedAnimationSpec] describing each repetition iteration.
+ * @param repeatMode whether animation should repeat by starting from the beginning (i.e.
+ * [RepeatMode.Restart]) or from the end (i.e. [RepeatMode.Reverse])
+ */
+class VectorizedInfiniteRepeatableSpec<V : AnimationVector>(
+ private val animation: VectorizedDurationBasedAnimationSpec<V>,
+ private val repeatMode: RepeatMode = RepeatMode.Restart
+) : VectorizedAnimationSpec<V> by
+ VectorizedRepeatableSpec<V>(InfiniteIterations, animation, repeatMode)
+
+/**
+ * This animation takes another [VectorizedDurationBasedAnimationSpec] and plays it
+ * [iterations] times. For infinitely repeating animation spec, [VectorizedInfiniteRepeatableSpec]
+ * is recommended.
+ *
+ * __Note__: When repeating in the [RepeatMode.Reverse] mode, it's highly recommended to have an
+ * __odd__ number of iterations. Otherwise, the animation may jump to the end value when it finishes
+ * the last iteration.
+ *
+ * @param iterations the count of iterations. Should be at least 1.
+ * @param animation the [VectorizedAnimationSpec] describing each repetition iteration.
+ * @param repeatMode whether animation should repeat by starting from the beginning (i.e.
+ * [RepeatMode.Restart]) or from the end (i.e. [RepeatMode.Reverse])
*/
class VectorizedRepeatableSpec<V : AnimationVector>(
private val iterations: Int,
private val animation: VectorizedDurationBasedAnimationSpec<V>,
private val repeatMode: RepeatMode = RepeatMode.Restart
-) : VectorizedAnimationSpec<V> {
+) : VectorizedFiniteAnimationSpec<V> {
init {
if (iterations < 1) {
@@ -345,15 +376,18 @@
* Stiffness constant for extremely stiff spring
*/
const val StiffnessHigh = 10_000f
+
/**
* Stiffness constant for medium stiff spring. This is the default stiffness for spring
* force.
*/
const val StiffnessMedium = 1500f
+
/**
* Stiffness constant for a spring with low stiffness.
*/
const val StiffnessLow = 200f
+
/**
* Stiffness constant for a spring with very low stiffness.
*/
@@ -364,23 +398,27 @@
* (i.e. damping ratio < 1), the lower the damping ratio, the more bouncy the spring.
*/
const val DampingRatioHighBouncy = 0.2f
+
/**
* Damping ratio for a medium bouncy spring. This is also the default damping ratio for
* spring force. Note for under-damped springs (i.e. damping ratio < 1), the lower the
* damping ratio, the more bouncy the spring.
*/
const val DampingRatioMediumBouncy = 0.5f
+
/**
* Damping ratio for a spring with low bounciness. Note for under-damped springs
* (i.e. damping ratio < 1), the lower the damping ratio, the higher the bounciness.
*/
const val DampingRatioLowBouncy = 0.75f
+
/**
* Damping ratio for a spring with no bounciness. This damping ratio will create a
* critically damped spring that returns to equilibrium within the shortest amount of time
* without oscillating.
*/
const val DampingRatioNoBouncy = 1f
+
/**
* Default cutoff for rounding off physics based animations
*/
@@ -401,7 +439,7 @@
val dampingRatio: Float,
val stiffness: Float,
anims: Animations
-) : VectorizedAnimationSpec<V> by VectorizedFloatAnimationSpec<V>(anims) {
+) : VectorizedFiniteAnimationSpec<V> by VectorizedFloatAnimationSpec<V>(anims) {
/**
* Creates a [VectorizedSpringSpec] that uses the same spring constants (i.e. [dampingRatio] and
@@ -482,7 +520,7 @@
*/
class VectorizedFloatAnimationSpec<V : AnimationVector> internal constructor(
private val anims: Animations
-) : VectorizedAnimationSpec<V> {
+) : VectorizedFiniteAnimationSpec<V> {
private lateinit var valueVector: V
private lateinit var velocityVector: V
private lateinit var endVelocityVector: V
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VisibilityThresholds.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VisibilityThresholds.kt
new file mode 100644
index 0000000..cc50e01
--- /dev/null
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/VisibilityThresholds.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.core
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.unit.Bounds
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.Position
+import androidx.compose.ui.unit.dp
+
+private const val DpVisibilityThreshold = 0.1f
+private const val PxVisibilityThreshold = 0.5f
+
+private val boundsVisibilityThreshold = Bounds(
+ Dp.VisibilityThreshold,
+ Dp.VisibilityThreshold,
+ Dp.VisibilityThreshold,
+ Dp.VisibilityThreshold
+)
+
+private val rectVisibilityThreshold = Rect(
+ PxVisibilityThreshold,
+ PxVisibilityThreshold,
+ PxVisibilityThreshold,
+ PxVisibilityThreshold
+)
+
+/**
+ * Visibility threshold for [IntOffset]. This defines the amount of value change that is
+ * considered to be no longer visible. The animation system uses this to signal to some default
+ * [spring] animations to stop when the value is close enough to the target.
+ */
+val IntOffset.Companion.VisibilityThreshold: IntOffset
+ get() = IntOffset(1, 1)
+
+/**
+ * Visibility threshold for [Offset]. This defines the amount of value change that is
+ * considered to be no longer visible. The animation system uses this to signal to some default
+ * [spring] animations to stop when the value is close enough to the target.
+ */
+val Offset.Companion.VisibilityThreshold: Offset
+ get() = Offset(PxVisibilityThreshold, PxVisibilityThreshold)
+
+/**
+ * Visibility threshold for [Int]. This defines the amount of value change that is
+ * considered to be no longer visible. The animation system uses this to signal to some default
+ * [spring] animations to stop when the value is close enough to the target.
+ */
+val Int.Companion.VisibilityThreshold: Int
+ get() = 1
+
+/**
+ * Visibility threshold for [Dp]. This defines the amount of value change that is
+ * considered to be no longer visible. The animation system uses this to signal to some default
+ * [spring] animations to stop when the value is close enough to the target.
+ */
+val Dp.Companion.VisibilityThreshold: Dp
+ get() = DpVisibilityThreshold.dp
+
+/**
+ * Visibility threshold for [Position]. This defines the amount of value change that is
+ * considered to be no longer visible. The animation system uses this to signal to some default
+ * [spring] animations to stop when the value is close enough to the target.
+ */
+val Position.Companion.VisibilityThreshold: Position
+ get() = Position(Dp.VisibilityThreshold, Dp.VisibilityThreshold)
+
+/**
+ * Visibility threshold for [Size]. This defines the amount of value change that is
+ * considered to be no longer visible. The animation system uses this to signal to some default
+ * [spring] animations to stop when the value is close enough to the target.
+ */
+val Size.Companion.VisibilityThreshold: Size
+ get() = Size(PxVisibilityThreshold, PxVisibilityThreshold)
+
+/**
+ * Visibility threshold for [IntSize]. This defines the amount of value change that is
+ * considered to be no longer visible. The animation system uses this to signal to some default
+ * [spring] animations to stop when the value is close enough to the target.
+ */
+val IntSize.Companion.VisibilityThreshold: IntSize
+ get() = IntSize(1, 1)
+
+/**
+ * Visibility threshold for [Rect]. This defines the amount of value change that is
+ * considered to be no longer visible. The animation system uses this to signal to some default
+ * [spring] animations to stop when the value is close enough to the target.
+ */
+val Rect.Companion.VisibilityThreshold: Rect
+ get() = rectVisibilityThreshold
+
+/**
+ * Visibility threshold for [Bounds]. This defines the amount of value change that is
+ * considered to be no longer visible. The animation system uses this to signal to some default
+ * [spring] animations to stop when the value is close enough to the target.
+ */
+val Bounds.Companion.VisibilityThreshold: Bounds
+ get() = boundsVisibilityThreshold
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/RepeatableAnimationTest.kt b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/RepeatableAnimationTest.kt
index 671941d..6524f1f 100644
--- a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/RepeatableAnimationTest.kt
+++ b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/RepeatableAnimationTest.kt
@@ -104,6 +104,25 @@
assertEquals(100f, repeatAnim.getValue(901))
}
+ @Test
+ fun testInfiniteRepeat() {
+ val repeat = infiniteRepeatable(
+ animation = TweenSpec<Float>(
+ durationMillis = 100, easing = LinearEasing
+ ),
+ repeatMode = RepeatMode.Reverse
+ )
+
+ assertEquals(
+ Int.MAX_VALUE.toLong() * 100,
+ repeat.vectorize(Float.VectorConverter).getDurationMillis(
+ AnimationVector(0f),
+ AnimationVector(100f),
+ AnimationVector(0f)
+ )
+ )
+ }
+
private companion object {
private val DelayDuration = 13
private val Duration = 50
diff --git a/compose/animation/animation/api/current.txt b/compose/animation/animation/api/current.txt
index d9e7a92..3922f7a 100644
--- a/compose/animation/animation/api/current.txt
+++ b/compose/animation/animation/api/current.txt
@@ -94,10 +94,6 @@
@kotlin.RequiresOptIn(message="This is an experimental animation API.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ExperimentalAnimationApi {
}
- public final class LegacyTransitionKt {
- method @Deprecated @androidx.compose.runtime.Composable public static <T> void Transition(androidx.compose.animation.core.TransitionDefinition<T> definition, T? toState, optional androidx.compose.animation.core.AnimationClockObservable clock, optional T? initState, optional kotlin.jvm.functions.Function1<? super T,kotlin.Unit>? onStateChangeFinished, kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.TransitionState,kotlin.Unit> children);
- }
-
public final class OffsetPropKey implements androidx.compose.animation.core.PropKey<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> {
ctor public OffsetPropKey(String label);
ctor public OffsetPropKey();
@@ -109,14 +105,14 @@
public final class PropertyKeysKt {
method public static kotlin.jvm.functions.Function1<androidx.compose.ui.graphics.colorspace.ColorSpace,androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.graphics.Color,androidx.compose.animation.core.AnimationVector4D>> getVectorConverter(androidx.compose.ui.graphics.Color.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Rect,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.geometry.Rect.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Dp,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(androidx.compose.ui.unit.Dp.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Position,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.Position.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Size,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Size.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Bounds,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.unit.Bounds.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Offset.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntOffset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntOffset.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntSize,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntSize.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Rect,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.geometry.Rect.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Dp,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(androidx.compose.ui.unit.Dp.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Position,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.Position.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Size,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Size.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Bounds,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.unit.Bounds.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Offset.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntOffset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntOffset.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntSize,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntSize.Companion);
}
public final class PxPropKey implements androidx.compose.animation.core.PropKey<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> {
@@ -154,6 +150,7 @@
}
public final class TransitionKt {
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> animateColor(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.graphics.Color>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.graphics.Color> targetValueByState);
method @Deprecated @VisibleForTesting public static void setTransitionsEnabled(boolean p);
method @androidx.compose.runtime.Composable public static <T> androidx.compose.animation.core.TransitionState transition(androidx.compose.animation.core.TransitionDefinition<T> definition, T? toState, optional androidx.compose.animation.core.AnimationClockObservable clock, optional T? initState, optional String? label, optional kotlin.jvm.functions.Function1<? super T,kotlin.Unit>? onStateChangeFinished);
}
diff --git a/compose/animation/animation/api/public_plus_experimental_current.txt b/compose/animation/animation/api/public_plus_experimental_current.txt
index d9e7a92..3922f7a 100644
--- a/compose/animation/animation/api/public_plus_experimental_current.txt
+++ b/compose/animation/animation/api/public_plus_experimental_current.txt
@@ -94,10 +94,6 @@
@kotlin.RequiresOptIn(message="This is an experimental animation API.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ExperimentalAnimationApi {
}
- public final class LegacyTransitionKt {
- method @Deprecated @androidx.compose.runtime.Composable public static <T> void Transition(androidx.compose.animation.core.TransitionDefinition<T> definition, T? toState, optional androidx.compose.animation.core.AnimationClockObservable clock, optional T? initState, optional kotlin.jvm.functions.Function1<? super T,kotlin.Unit>? onStateChangeFinished, kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.TransitionState,kotlin.Unit> children);
- }
-
public final class OffsetPropKey implements androidx.compose.animation.core.PropKey<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> {
ctor public OffsetPropKey(String label);
ctor public OffsetPropKey();
@@ -109,14 +105,14 @@
public final class PropertyKeysKt {
method public static kotlin.jvm.functions.Function1<androidx.compose.ui.graphics.colorspace.ColorSpace,androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.graphics.Color,androidx.compose.animation.core.AnimationVector4D>> getVectorConverter(androidx.compose.ui.graphics.Color.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Rect,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.geometry.Rect.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Dp,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(androidx.compose.ui.unit.Dp.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Position,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.Position.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Size,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Size.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Bounds,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.unit.Bounds.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Offset.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntOffset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntOffset.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntSize,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntSize.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Rect,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.geometry.Rect.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Dp,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(androidx.compose.ui.unit.Dp.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Position,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.Position.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Size,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Size.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Bounds,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.unit.Bounds.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Offset.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntOffset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntOffset.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntSize,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntSize.Companion);
}
public final class PxPropKey implements androidx.compose.animation.core.PropKey<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> {
@@ -154,6 +150,7 @@
}
public final class TransitionKt {
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> animateColor(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.graphics.Color>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.graphics.Color> targetValueByState);
method @Deprecated @VisibleForTesting public static void setTransitionsEnabled(boolean p);
method @androidx.compose.runtime.Composable public static <T> androidx.compose.animation.core.TransitionState transition(androidx.compose.animation.core.TransitionDefinition<T> definition, T? toState, optional androidx.compose.animation.core.AnimationClockObservable clock, optional T? initState, optional String? label, optional kotlin.jvm.functions.Function1<? super T,kotlin.Unit>? onStateChangeFinished);
}
diff --git a/compose/animation/animation/api/restricted_current.txt b/compose/animation/animation/api/restricted_current.txt
index d9e7a92..3922f7a 100644
--- a/compose/animation/animation/api/restricted_current.txt
+++ b/compose/animation/animation/api/restricted_current.txt
@@ -94,10 +94,6 @@
@kotlin.RequiresOptIn(message="This is an experimental animation API.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ExperimentalAnimationApi {
}
- public final class LegacyTransitionKt {
- method @Deprecated @androidx.compose.runtime.Composable public static <T> void Transition(androidx.compose.animation.core.TransitionDefinition<T> definition, T? toState, optional androidx.compose.animation.core.AnimationClockObservable clock, optional T? initState, optional kotlin.jvm.functions.Function1<? super T,kotlin.Unit>? onStateChangeFinished, kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.TransitionState,kotlin.Unit> children);
- }
-
public final class OffsetPropKey implements androidx.compose.animation.core.PropKey<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> {
ctor public OffsetPropKey(String label);
ctor public OffsetPropKey();
@@ -109,14 +105,14 @@
public final class PropertyKeysKt {
method public static kotlin.jvm.functions.Function1<androidx.compose.ui.graphics.colorspace.ColorSpace,androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.graphics.Color,androidx.compose.animation.core.AnimationVector4D>> getVectorConverter(androidx.compose.ui.graphics.Color.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Rect,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.geometry.Rect.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Dp,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(androidx.compose.ui.unit.Dp.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Position,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.Position.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Size,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Size.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Bounds,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.unit.Bounds.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Offset.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntOffset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntOffset.Companion);
- method public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntSize,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntSize.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Rect,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.geometry.Rect.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Dp,androidx.compose.animation.core.AnimationVector1D> getVectorConverter(androidx.compose.ui.unit.Dp.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Position,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.Position.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Size,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Size.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.Bounds,androidx.compose.animation.core.AnimationVector4D> getVectorConverter(androidx.compose.ui.unit.Bounds.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.geometry.Offset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.geometry.Offset.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntOffset,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntOffset.Companion);
+ method @Deprecated public static androidx.compose.animation.core.TwoWayConverter<androidx.compose.ui.unit.IntSize,androidx.compose.animation.core.AnimationVector2D> getVectorConverter(androidx.compose.ui.unit.IntSize.Companion);
}
public final class PxPropKey implements androidx.compose.animation.core.PropKey<java.lang.Float,androidx.compose.animation.core.AnimationVector1D> {
@@ -154,6 +150,7 @@
}
public final class TransitionKt {
+ method @androidx.compose.runtime.Composable public static inline <S> androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> animateColor(androidx.compose.animation.core.Transition<S>, optional kotlin.jvm.functions.Function1<? super androidx.compose.animation.core.Transition.States<S>,? extends androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.graphics.Color>> transitionSpec, kotlin.jvm.functions.Function1<? super S,androidx.compose.ui.graphics.Color> targetValueByState);
method @Deprecated @VisibleForTesting public static void setTransitionsEnabled(boolean p);
method @androidx.compose.runtime.Composable public static <T> androidx.compose.animation.core.TransitionState transition(androidx.compose.animation.core.TransitionDefinition<T> definition, T? toState, optional androidx.compose.animation.core.AnimationClockObservable clock, optional T? initState, optional String? label, optional kotlin.jvm.functions.Function1<? super T,kotlin.Unit>? onStateChangeFinished);
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/build.gradle b/compose/animation/animation/integration-tests/animation-demos/build.gradle
index 043cb3a..feac8d4 100644
--- a/compose/animation/animation/integration-tests/animation-demos/build.gradle
+++ b/compose/animation/animation/integration-tests/animation-demos/build.gradle
@@ -21,6 +21,7 @@
implementation project(":compose:ui:ui-text")
implementation project(':compose:animation:animation')
implementation project(':compose:animation:animation:animation-samples')
+ implementation project(':compose:animation:animation-core:animation-core-samples')
implementation project(':compose:foundation:foundation')
implementation project(':compose:material:material')
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimatedVisiblilityLazyColumnDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimatedVisiblilityLazyColumnDemo.kt
index d28cb90..2d1b4b4 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimatedVisiblilityLazyColumnDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimatedVisiblilityLazyColumnDemo.kt
@@ -27,7 +27,7 @@
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumnForIndexed
+import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@@ -62,13 +62,15 @@
Text("Remove")
}
}
- LazyColumnForIndexed(turquoiseColors) { i, color ->
- AnimatedVisibility(
- (turquoiseColors.size - itemNum) <= i,
- enter = expandVertically(),
- exit = shrinkVertically()
- ) {
- Spacer(Modifier.fillMaxWidth().height(90.dp).background(color))
+ LazyColumn {
+ itemsIndexed(turquoiseColors) { i, color ->
+ AnimatedVisibility(
+ (turquoiseColors.size - itemNum) <= i,
+ enter = expandVertically(),
+ exit = shrinkVertically()
+ ) {
+ Spacer(Modifier.fillMaxWidth().height(90.dp).background(color))
+ }
}
}
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 1dbecc6..59b6638 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
@@ -26,9 +26,6 @@
"State Transition Demos",
listOf(
ComposableDemo("Multi-dimensional prop") { MultiDimensionalAnimationDemo() },
- ComposableDemo("State animation with interruptions") {
- StateAnimationWithInterruptionsDemo()
- },
ComposableDemo("State based ripple") { StateBasedRippleDemo() },
ComposableDemo("Repeating rotation") { RepeatedRotationDemo() },
ComposableDemo("Manual animation clock") { AnimatableSeekBarDemo() },
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/EnterExitTransitionDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/EnterExitTransitionDemo.kt
index d5b30d9..4e85b17 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/EnterExitTransitionDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/EnterExitTransitionDemo.kt
@@ -37,7 +37,7 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.preferredHeight
import androidx.compose.foundation.layout.wrapContentWidth
-import androidx.compose.foundation.lazy.LazyColumnFor
+import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.Button
import androidx.compose.material.Checkbox
@@ -224,11 +224,10 @@
}
}
}
- LazyColumnFor(
- menuText,
- modifier = Modifier.fillMaxSize().background(Color(0xFFd8c7ff))
- ) {
- Text(it, Modifier.padding(5.dp))
+ LazyColumn(Modifier.fillMaxSize().background(Color(0xFFd8c7ff))) {
+ items(menuText) {
+ Text(it, Modifier.padding(5.dp))
+ }
}
}
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/GestureBasedAnimationDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/GestureBasedAnimationDemo.kt
index 734e749..d0801a5 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/GestureBasedAnimationDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/GestureBasedAnimationDemo.kt
@@ -16,69 +16,10 @@
package androidx.compose.animation.demos
-import androidx.compose.animation.ColorPropKey
-import androidx.compose.animation.core.FloatPropKey
-import androidx.compose.animation.core.spring
-import androidx.compose.animation.core.transitionDefinition
-import androidx.compose.animation.transition
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.animation.core.samples.GestureAnimationSample
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.gesture.pressIndicatorGestureFilter
-import androidx.compose.ui.graphics.Color
-
-private const val halfSize = 200f
-
-private enum class ComponentState { Pressed, Released }
-
-private val scale = FloatPropKey()
-private val color = ColorPropKey()
-
-private val definition = transitionDefinition<ComponentState> {
- state(ComponentState.Released) {
- this[scale] = 1f
- this[color] = Color(red = 0, green = 200, blue = 0, alpha = 255)
- }
- state(ComponentState.Pressed) {
- this[scale] = 3f
- this[color] = Color(red = 0, green = 100, blue = 0, alpha = 255)
- }
- transition {
- scale using spring(
- stiffness = 50f
- )
- color using spring(
- stiffness = 50f
- )
- }
-}
@Composable
fun GestureBasedAnimationDemo() {
- val toState = remember { mutableStateOf(ComponentState.Released) }
- val pressIndicator =
- Modifier.pressIndicatorGestureFilter(
- onStart = { toState.value = ComponentState.Pressed },
- onStop = { toState.value = ComponentState.Released },
- onCancel = { toState.value = ComponentState.Released }
- )
-
- val state = transition(definition = definition, toState = toState.value)
- ScaledColorRect(pressIndicator, scale = state[scale], color = state[color])
-}
-
-@Composable
-private fun ScaledColorRect(modifier: Modifier = Modifier, scale: Float, color: Color) {
- Canvas(modifier.fillMaxSize()) {
- drawRect(
- color,
- topLeft = Offset(center.x - halfSize * scale, center.y - halfSize * scale),
- size = Size(halfSize * 2 * scale, halfSize * 2 * scale)
- )
- }
+ GestureAnimationSample()
}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/InfiniteAnimationDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/InfiniteAnimationDemo.kt
index 0070e96..e0aa04d 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/InfiniteAnimationDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/InfiniteAnimationDemo.kt
@@ -16,10 +16,9 @@
package androidx.compose.animation.demos
-import androidx.compose.animation.core.AnimationConstants
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animate
-import androidx.compose.animation.core.repeatable
+import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -42,8 +41,7 @@
animate(
initialValue = 1f,
targetValue = 0f,
- animationSpec = repeatable(
- iterations = AnimationConstants.Infinite,
+ animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
)
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/MultiDimensionalAnimationDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/MultiDimensionalAnimationDemo.kt
index a54d5b7..eb04f22 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/MultiDimensionalAnimationDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/MultiDimensionalAnimationDemo.kt
@@ -16,18 +16,19 @@
package androidx.compose.animation.demos
-import androidx.compose.animation.ColorPropKey
-import androidx.compose.animation.RectPropKey
+import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.animateRect
import androidx.compose.animation.core.spring
-import androidx.compose.animation.core.transitionDefinition
import androidx.compose.animation.core.tween
-import androidx.compose.animation.transition
+import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxSize
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.geometry.Offset
import androidx.compose.ui.geometry.Rect
@@ -36,33 +37,42 @@
@Composable
fun MultiDimensionalAnimationDemo() {
- val currentState = remember { mutableStateOf(AnimState.Collapsed) }
+ var currentState by remember { mutableStateOf(AnimState.Collapsed) }
val onClick = {
// Cycle through states when clicked.
- currentState.value = when (currentState.value) {
+ currentState = when (currentState) {
AnimState.Collapsed -> AnimState.Expanded
AnimState.Expanded -> AnimState.PutAway
AnimState.PutAway -> AnimState.Collapsed
}
}
- val width = remember { mutableStateOf(0f) }
- val height = remember { mutableStateOf(0f) }
- val state = transition(
- definition = remember(width.value, height.value) {
- createTransDef(width.value, height.value)
- },
- toState = currentState.value
- )
- Canvas(modifier = Modifier.fillMaxSize().clickable(onClick = onClick, indication = null)) {
- width.value = size.width
- height.value = size.height
+ var width by remember { mutableStateOf(0f) }
+ var height by remember { mutableStateOf(0f) }
+ val transition = updateTransition(currentState)
+ val rect by transition.animateRect({ spring(stiffness = 100f) }) {
+ when (it) {
+ AnimState.Collapsed -> Rect(600f, 600f, 900f, 900f)
+ AnimState.Expanded -> Rect(0f, 400f, width, height - 400f)
+ AnimState.PutAway -> Rect(width - 300f, height - 300f, width, height)
+ }
+ }
- val bounds = state[bounds]
+ val color by transition.animateColor(transitionSpec = { tween(durationMillis = 500) }) {
+ when (it) {
+ AnimState.Collapsed -> Color.LightGray
+ AnimState.Expanded -> Color(0xFFd0fff8)
+ AnimState.PutAway -> Color(0xFFe3ffd9)
+ }
+ }
+ Canvas(modifier = Modifier.fillMaxSize().clickable(onClick = onClick, indication = null)) {
+ width = size.width
+ height = size.height
+
drawRect(
- state[background],
- topLeft = Offset(bounds.left, bounds.top),
- size = Size(bounds.width, bounds.height)
+ color,
+ topLeft = Offset(rect.left, rect.top),
+ size = Size(rect.width, rect.height)
)
}
}
@@ -71,36 +81,4 @@
Collapsed,
Expanded,
PutAway
-}
-
-// Both PropKeys below are multi-dimensional property keys. That means each dimension's
-// value and velocity will be tracked independently. In the case of a color, each color
-// channel is a separate dimension. For rectangles, the dimensions are: top, left,
-// right and bottom.
-private val background = ColorPropKey()
-private val bounds = RectPropKey()
-
-private fun createTransDef(width: Float, height: Float) =
- transitionDefinition<AnimState> {
- state(AnimState.Collapsed) {
- this[background] = Color.LightGray
- this[bounds] = Rect(600f, 600f, 900f, 900f)
- }
- state(AnimState.Expanded) {
- this[background] = Color(0xFFd0fff8)
- this[bounds] = Rect(0f, 400f, width, height - 400f)
- }
- state(AnimState.PutAway) {
- this[background] = Color(0xFFe3ffd9)
- this[bounds] = Rect(width - 300f, height - 300f, width, height)
- }
-
- transition {
- bounds using spring(
- stiffness = 100f
- )
- background using tween(
- durationMillis = 500
- )
- }
- }
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/RepeatedRotationDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/RepeatedRotationDemo.kt
index 5917cc2..c365c64 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/RepeatedRotationDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/RepeatedRotationDemo.kt
@@ -16,31 +16,32 @@
package androidx.compose.animation.demos
-import androidx.compose.animation.core.FloatPropKey
import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.repeatable
-import androidx.compose.animation.core.transitionDefinition
+import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.core.tween
-import androidx.compose.animation.transition
import androidx.compose.foundation.Canvas
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.height
import androidx.compose.foundation.layout.preferredSize
import androidx.compose.foundation.layout.wrapContentSize
+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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.tapGestureFilter
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.rotate
-import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
@Composable
fun RepeatedRotationDemo() {
@@ -50,23 +51,39 @@
.wrapContentSize(Alignment.Center),
verticalArrangement = Arrangement.SpaceEvenly
) {
- val textStyle = TextStyle(fontSize = 18.sp)
- Text(
- modifier = Modifier.tapGestureFilter(onTap = { state.value = RotationStates.Rotated }),
- text = "Rotate 10 times",
- style = textStyle
- )
- Text(
- modifier = Modifier.tapGestureFilter(onTap = { state.value = RotationStates.Original }),
- text = "Reset",
- style = textStyle
- )
- val transitionState = transition(
- definition = definition,
- toState = state.value
- )
+ Button(
+ { state.value = RotationStates.Rotated }
+ ) {
+ Text(text = "Rotate 10 times")
+ }
+ Spacer(Modifier.height(10.dp))
+ Button(
+ { state.value = RotationStates.Original }
+ ) {
+ Text(text = "Reset")
+ }
+ Spacer(Modifier.height(10.dp))
+ val transition = updateTransition(state.value)
+ val rotation by transition.animateFloat(
+ {
+ if (it.initialState == RotationStates.Original) {
+ repeatable(
+ iterations = 10,
+ animation = keyframes {
+ durationMillis = 1000
+ 0f at 0 with LinearEasing
+ 360f at 1000
+ }
+ )
+ } else {
+ tween(durationMillis = 300)
+ }
+ }
+ ) {
+ 0f
+ }
Canvas(Modifier.preferredSize(100.dp)) {
- rotate(transitionState[rotation], Offset.Zero) {
+ rotate(rotation, Offset.Zero) {
drawRect(Color(0xFF00FF00))
}
}
@@ -76,29 +93,4 @@
private enum class RotationStates {
Original,
Rotated
-}
-
-private val rotation = FloatPropKey()
-
-private val definition = transitionDefinition<RotationStates> {
- state(RotationStates.Original) {
- this[rotation] = 0f
- }
- state(RotationStates.Rotated) {
- this[rotation] = 360f
- }
- transition(RotationStates.Original to RotationStates.Rotated) {
- rotation using repeatable(
- iterations = 10,
- animation = tween(
- easing = LinearEasing,
- durationMillis = 1000
- )
- )
- }
- transition(RotationStates.Rotated to RotationStates.Original) {
- rotation using tween(
- durationMillis = 300
- )
- }
-}
+}
\ No newline at end of file
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringBackScrollingDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringBackScrollingDemo.kt
index 9d70c16..4455664 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringBackScrollingDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringBackScrollingDemo.kt
@@ -42,7 +42,6 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.gesture.ExperimentalPointerInput
import androidx.compose.ui.gesture.util.VelocityTracker
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
@@ -54,7 +53,6 @@
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
-@OptIn(ExperimentalPointerInput::class)
@Composable
fun SpringBackScrollingDemo() {
Column(Modifier.fillMaxHeight()) {
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/StateAnimationWithInterruptionsDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/StateAnimationWithInterruptionsDemo.kt
deleted file mode 100644
index cd5d8f6..0000000
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/StateAnimationWithInterruptionsDemo.kt
+++ /dev/null
@@ -1,109 +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.compose.animation.demos
-
-import android.os.Handler
-import android.os.Looper
-import androidx.compose.animation.ColorPropKey
-import androidx.compose.animation.core.FloatPropKey
-import androidx.compose.animation.core.TransitionState
-import androidx.compose.animation.core.spring
-import androidx.compose.animation.core.transitionDefinition
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.transition
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.Color
-
-@Composable
-fun StateAnimationWithInterruptionsDemo() {
- Box(Modifier.fillMaxSize()) {
- ColorRect()
- }
-}
-
-private val background = ColorPropKey()
-private val y = FloatPropKey()
-
-private enum class OverlayState {
- Open,
- Closed
-}
-
-private val definition = transitionDefinition<OverlayState> {
- state(OverlayState.Open) {
- this[background] = Color(red = 128, green = 128, blue = 128, alpha = 255)
- this[y] = 1f // percentage
- }
- state(OverlayState.Closed) {
- this[background] = Color(red = 188, green = 222, blue = 145, alpha = 255)
- this[y] = 0f // percentage
- }
- // Apply this transition to all state changes (i.e. Open -> Closed and Closed -> Open)
- transition {
- background using tween(
- durationMillis = 800
- )
- y using spring(
- // Extremely low stiffness
- stiffness = 40f
- )
- }
-}
-
-private val handler = Handler(Looper.getMainLooper())
-
-@Composable
-private fun ColorRect() {
- var toState by mutableStateOf(OverlayState.Closed)
- handler.postDelayed(
- object : Runnable {
- override fun run() {
- if ((0..1).random() == 0) {
- toState = OverlayState.Open
- } else {
- toState = OverlayState.Closed
- }
- }
- },
- (200..800).random().toLong()
- )
- val state = transition(definition = definition, toState = toState)
- ColorRectState(state = state)
-}
-
-@Composable
-private fun ColorRectState(state: TransitionState) {
- val color = state[background]
- val scaleY = state[y]
- Canvas(Modifier.fillMaxSize().background(color = color)) {
- drawRect(
- Color(alpha = 255, red = 255, green = 255, blue = 255),
- topLeft = Offset(100f, 0f),
- size = Size(size.width - 200f, scaleY * size.height)
- )
- }
-}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/StateBasedRippleDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/StateBasedRippleDemo.kt
index 03c4319..917bcd8 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/StateBasedRippleDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/StateBasedRippleDemo.kt
@@ -16,26 +16,25 @@
package androidx.compose.animation.demos
-import android.graphics.PointF
-import androidx.compose.animation.core.FloatPropKey
-import androidx.compose.animation.core.InterruptionHandling
-import androidx.compose.animation.core.TransitionDefinition
-import androidx.compose.animation.core.TransitionState
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.animateDp
+import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.keyframes
-import androidx.compose.animation.core.transitionDefinition
+import androidx.compose.animation.core.snap
+import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.core.tween
-import androidx.compose.animation.transition
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
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.geometry.Offset
import androidx.compose.ui.gesture.pressIndicatorGestureFilter
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.AmbientDensity
import androidx.compose.ui.unit.dp
@Composable
@@ -47,37 +46,76 @@
@Composable
private fun RippleRect() {
- val radius = with(AmbientDensity.current) { TargetRadius.toPx() }
- val toState = remember { mutableStateOf(ButtonStatus.Initial) }
- val rippleTransDef = remember { createTransDef(radius) }
+ var down by remember { mutableStateOf(Offset(0f, 0f)) }
+ var toState by remember { mutableStateOf(ButtonStatus.Initial) }
val onPress: (Offset) -> Unit = { position ->
- down.x = position.x
- down.y = position.y
- toState.value = ButtonStatus.Pressed
+ down = position
+ toState = ButtonStatus.Pressed
}
val onRelease: () -> Unit = {
- toState.value = ButtonStatus.Released
+ toState = ButtonStatus.Released
}
- val state = transition(definition = rippleTransDef, toState = toState.value)
+ val transition = updateTransition(toState)
RippleRectFromState(
- Modifier.pressIndicatorGestureFilter(onStart = onPress, onStop = onRelease), state = state
+ Modifier.pressIndicatorGestureFilter(onStart = onPress, onStop = onRelease),
+ center = down,
+ transition = transition
)
}
@Composable
-private fun RippleRectFromState(modifier: Modifier = Modifier, state: TransitionState) {
+private fun RippleRectFromState(
+ modifier: Modifier = Modifier,
+ center: Offset,
+ transition: Transition<ButtonStatus>
+) {
+ // TODO: Initial -> Pressed: Uninterruptible
+ // TODO: Pressed -> Released: Uninterruptible
+ // TODO: Auto transition to Initial
+ val alpha by transition.animateFloat(
+ transitionSpec = {
+ if (it.initialState == ButtonStatus.Initial && it.targetState == ButtonStatus.Pressed) {
+ keyframes {
+ durationMillis = 225
+ 0f at 0 // optional
+ 0.2f at 75
+ 0.2f at 225 // optional
+ }
+ } else if (it.initialState == ButtonStatus.Pressed &&
+ it.targetState == ButtonStatus.Released
+ ) {
+ tween(durationMillis = 220)
+ } else {
+ snap()
+ }
+ }
+ ) {
+ if (it == ButtonStatus.Pressed) 0.2f else 0f
+ }
+
+ val radius by transition.animateDp(
+ transitionSpec = {
+ if (it.initialState == ButtonStatus.Initial && it.targetState == ButtonStatus.Pressed) {
+ tween(225)
+ } else {
+ snap()
+ }
+ }
+ ) {
+ if (it == ButtonStatus.Initial) TargetRadius * 0.3f else TargetRadius + 15.dp
+ }
+
Canvas(modifier.fillMaxSize()) {
- // TODO: file bug for when "down" is not a file level val, it's not memoized correctly
drawCircle(
Color(
- alpha = (state[alpha] * 255).toInt(),
+ alpha = (alpha * 255).toInt(),
red = 0,
green = 235,
blue = 224
),
- center = Offset(down.x, down.y),
- radius = state[radius]
+ center = center,
+ radius = radius.toPx()
)
}
}
@@ -88,49 +126,4 @@
Released
}
-private val TargetRadius = 200.dp
-
-private val down = PointF(0f, 0f)
-
-private val alpha = FloatPropKey()
-private val radius = FloatPropKey()
-
-private fun createTransDef(targetRadius: Float): TransitionDefinition<ButtonStatus> {
- return transitionDefinition {
- state(ButtonStatus.Initial) {
- this[alpha] = 0f
- this[radius] = targetRadius * 0.3f
- }
- state(ButtonStatus.Pressed) {
- this[alpha] = 0.2f
- this[radius] = targetRadius + 15f
- }
- state(ButtonStatus.Released) {
- this[alpha] = 0f
- this[radius] = targetRadius + 15f
- }
-
- // Grow the ripple
- transition(ButtonStatus.Initial to ButtonStatus.Pressed) {
- alpha using keyframes {
- durationMillis = 225
- 0f at 0 // optional
- 0.2f at 75
- 0.2f at 225 // optional
- }
- radius using tween(durationMillis = 225)
- interruptionHandling = InterruptionHandling.UNINTERRUPTIBLE
- }
-
- // Fade out the ripple
- transition(ButtonStatus.Pressed to ButtonStatus.Released) {
- alpha using tween(durationMillis = 200)
- interruptionHandling = InterruptionHandling.UNINTERRUPTIBLE
- // switch back to Initial to prepare for the next ripple cycle
- nextState = ButtonStatus.Initial
- }
-
- // State switch without animation
- snapTransition(ButtonStatus.Released to ButtonStatus.Initial)
- }
-}
+private val TargetRadius = 200.dp
\ No newline at end of file
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SupendAnimationDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SupendAnimationDemo.kt
index 4045fb6..36e0942 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SupendAnimationDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SupendAnimationDemo.kt
@@ -37,14 +37,12 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.gesture.ExperimentalPointerInput
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
-@OptIn(ExperimentalPointerInput::class)
@Composable
fun SuspendAnimationDemo() {
var animStateX by remember {
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SwipeToDismissDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SwipeToDismissDemo.kt
index 1e5ef0d..289b452 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SwipeToDismissDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SwipeToDismissDemo.kt
@@ -42,7 +42,6 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
-import androidx.compose.ui.gesture.ExperimentalPointerInput
import androidx.compose.ui.gesture.util.VelocityTracker
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
@@ -58,9 +57,10 @@
fun SwipeToDismissDemo() {
Column {
var index by remember { mutableStateOf(0) }
- Box(Modifier.height(500.dp).fillMaxWidth()) {
+ val dismissState = remember { DismissState() }
+ Box(Modifier.height(300.dp).fillMaxWidth()) {
Box(
- Modifier.swipeToDismiss(index).align(Alignment.BottomCenter).size(300.dp)
+ Modifier.swipeToDismiss(dismissState).align(Alignment.BottomCenter).size(150.dp)
.background(pastelColors[index])
)
}
@@ -72,6 +72,8 @@
Button(
onClick = {
index = (index + 1) % pastelColors.size
+ dismissState.alpha = 1f
+ dismissState.offset = 0f
},
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
@@ -80,21 +82,13 @@
}
}
-@OptIn(ExperimentalPointerInput::class)
-private fun Modifier.swipeToDismiss(index: Int): Modifier = composed {
+private fun Modifier.swipeToDismiss(dismissState: DismissState): Modifier = composed {
val mutatorMutex = remember { MutatorMutex() }
- var alpha by remember { mutableStateOf(1f) }
- var offset by remember { mutableStateOf(0f) }
- remember(index) {
- // Reset internal states if index has been updated
- alpha = 1f
- offset = 0f
- }
this.pointerInput {
fun updateOffset(value: Float) {
- offset = value
- alpha = 1f - abs(offset / size.height)
+ dismissState.offset = value
+ dismissState.alpha = 1f - abs(dismissState.offset / size.height)
}
coroutineScope {
while (true) {
@@ -106,7 +100,7 @@
mutatorMutex.mutate(MutatePriority.UserInput) {
handlePointerInput {
verticalDrag(pointerId) {
- updateOffset(offset + it.positionChange().y)
+ updateOffset(dismissState.offset + it.positionChange().y)
velocityTracker.addPosition(
it.current.uptime,
it.current.position
@@ -120,9 +114,9 @@
// animation job.
mutatorMutex.mutate {
// Either fling out of the sight, or snap back
- val animationState = AnimationState(offset, velocity)
+ val animationState = AnimationState(dismissState.offset, velocity)
val decay = AndroidFlingDecaySpec(this@pointerInput)
- if (decay.getTarget(offset, velocity) >= -size.height) {
+ if (decay.getTarget(dismissState.offset, velocity) >= -size.height) {
// Not enough velocity to be dismissed
animationState.animateTo(0f) {
updateOffset(value)
@@ -142,7 +136,12 @@
}
}
}
- }.offset(y = { offset }).graphicsLayer(alpha = alpha)
+ }.offset(y = { dismissState.offset }).graphicsLayer(alpha = dismissState.alpha)
+}
+
+private class DismissState {
+ var alpha by mutableStateOf(1f)
+ var offset by mutableStateOf(0f)
}
internal val pastelColors = listOf(
diff --git a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/SingleValueAnimationTest.kt b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/SingleValueAnimationTest.kt
index 8586b3c..e1e0cff 100644
--- a/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/SingleValueAnimationTest.kt
+++ b/compose/animation/animation/src/androidAndroidTest/kotlin/androidx/compose/animation/SingleValueAnimationTest.kt
@@ -22,6 +22,7 @@
import androidx.compose.animation.core.FloatSpringSpec
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.TweenSpec
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
@@ -338,14 +339,14 @@
var boundsValue = Bounds(0.dp, 0.dp, 0.dp, 0.dp)
val specForFloat = FloatSpringSpec(visibilityThreshold = 0.01f)
- val specForVector = FloatSpringSpec(visibilityThreshold = PxVisibilityThreshold)
- val specForOffset = FloatSpringSpec(visibilityThreshold = PxVisibilityThreshold)
- val specForBounds = FloatSpringSpec(visibilityThreshold = DpVisibilityThreshold)
+ val specForVector = FloatSpringSpec(visibilityThreshold = 0.5f)
+ val specForOffset = FloatSpringSpec(visibilityThreshold = 0.5f)
+ val specForBounds = FloatSpringSpec(visibilityThreshold = 0.1f)
val content: @Composable (Boolean) -> Unit = { enabled ->
vectorValue = animate(
if (enabled) AnimationVector(100f) else AnimationVector(0f),
- visibilityThreshold = AnimationVector(PxVisibilityThreshold)
+ visibilityThreshold = AnimationVector(0.5f)
)
offsetValue = animate(
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedVisibility.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedVisibility.kt
index 9b7b320..7772778 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedVisibility.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimatedVisibility.kt
@@ -25,8 +25,8 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
-import androidx.compose.ui.layout.Layout
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.AmbientAnimationClock
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt
index 132dfda..e870d7c 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/AnimationModifier.kt
@@ -21,6 +21,7 @@
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.AnimationVector2D
import androidx.compose.animation.core.SpringSpec
+import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.spring
import androidx.compose.runtime.remember
import androidx.compose.ui.layout.LayoutModifier
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
index 795d9a5..3f4b536 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
@@ -20,6 +20,7 @@
import androidx.compose.animation.core.AnimationEndReason
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.AnimationVector2D
+import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.spring
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/LegacyTransition.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/LegacyTransition.kt
deleted file mode 100644
index fcfad1b..0000000
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/LegacyTransition.kt
+++ /dev/null
@@ -1,81 +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.compose.animation
-
-import androidx.compose.animation.core.AnimationClockObservable
-import androidx.compose.animation.core.TransitionDefinition
-import androidx.compose.animation.core.TransitionState
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.platform.AmbientAnimationClock
-
-/**
- * __Deprecated:__ [Transition] has been deprecated. Please use [transition] instead.
- *
- * [Transition] composable creates a state-based transition using the animation configuration
- * defined in [TransitionDefinition]. This can be especially useful when animating multiple
- * values from a predefined set of values to another. For animating a single value, consider using
- * [animatedValue], [animatedFloat], [animatedColor] or the more light-weight [animate] APIs.
- *
- * [Transition] starts a new animation or changes the on-going animation when the [toState]
- * parameter is changed to a different value. It dutifully ensures that the animation will head
- * towards new [toState] regardless of what state (or in-between state) it’s currently in: If the
- * transition is not currently animating, having a new [toState] value will start a new animation,
- * otherwise the in-flight animation will correct course and animate towards the new [toState]
- * based on the interruption handling logic.
- *
- * [Transition] takes a transition definition, a target state and child composables.
- * These child composables will be receiving a [TransitionState] object as an argument, which
- * captures all the current values of the animation. Child composables should read the animation
- * values from the [TransitionState] object, and apply the value wherever necessary.
- *
- * @sample androidx.compose.animation.samples.TransitionSample
- *
- * @param definition Transition definition that defines states and transitions
- * @param toState New state to transition to
- * @param clock Optional animation clock that pulses animations when time changes. By default,
- * the system uses a choreographer based clock read from the [AnimationClockAmbient].
- * A custom implementation of the [AnimationClockObservable] (such as a
- * [androidx.compose.animation.core.ManualAnimationClock]) can be supplied here if there’s a need to
- * manually control the clock (for example in tests).
- * @param initState Optional initial state for the transition. When undefined, the initial state
- * will be set to the first [toState] seen in the transition.
- * @param onStateChangeFinished An optional listener to get notified when state change animation
- * has completed
- * @param children The children composables that will be animated
- *
- * @see [TransitionDefinition]
- */
-@Deprecated(
- "Transition has been renamed to transition, which returns a TransitionState instead " +
- "of passing it to children",
- replaceWith = ReplaceWith(
- "transition(definition, toState, clock, initState, null, onStateChangeFinished)",
- "androidx.compose.animation.transition"
- )
-)
-@Composable
-fun <T> Transition(
- definition: TransitionDefinition<T>,
- toState: T,
- clock: AnimationClockObservable = AmbientAnimationClock.current,
- initState: T = toState,
- onStateChangeFinished: ((T) -> Unit)? = null,
- children: @Composable (state: TransitionState) -> Unit
-) {
- val state = transition(definition, toState, clock, initState, null, onStateChangeFinished)
- children(state)
-}
\ No newline at end of file
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/PropertyKeys.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/PropertyKeys.kt
index bfc1b0d..12cc147 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/PropertyKeys.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/PropertyKeys.kt
@@ -204,47 +204,55 @@
/**
* A type converter that converts a [Rect] to a [AnimationVector4D], and vice versa.
*/
+@Deprecated("Rect.VectorConverter has been moved to animation-core library")
val Rect.Companion.VectorConverter: TwoWayConverter<Rect, AnimationVector4D>
get() = RectToVector
/**
* A type converter that converts a [Dp] to a [AnimationVector1D], and vice versa.
*/
+@Deprecated("Dp.VectorConverter has been moved to animation-core library")
val Dp.Companion.VectorConverter: TwoWayConverter<Dp, AnimationVector1D>
get() = DpToVector
/**
* A type converter that converts a [Position] to a [AnimationVector2D], and vice versa.
*/
+@Deprecated("Position.VectorConverter has been moved to animation-core library")
val Position.Companion.VectorConverter: TwoWayConverter<Position, AnimationVector2D>
get() = PositionToVector
/**
* A type converter that converts a [Size] to a [AnimationVector2D], and vice versa.
*/
+@Deprecated("Size.VectorConverter has been moved to animation-core library")
val Size.Companion.VectorConverter: TwoWayConverter<Size, AnimationVector2D>
get() = SizeToVector
/**
* A type converter that converts a [Bounds] to a [AnimationVector4D], and vice versa.
*/
+@Deprecated("Bounds.VectorConverter has been moved to animation-core library")
val Bounds.Companion.VectorConverter: TwoWayConverter<Bounds, AnimationVector4D>
get() = BoundsToVector
/**
* A type converter that converts a [Offset] to a [AnimationVector2D], and vice versa.
*/
+@Deprecated("Offset.VectorConverter has been moved to animation-core library")
val Offset.Companion.VectorConverter: TwoWayConverter<Offset, AnimationVector2D>
get() = OffsetToVector
/**
* A type converter that converts a [IntOffset] to a [AnimationVector2D], and vice versa.
*/
+@Deprecated("IntOffset.VectorConverter has been moved to animation-core library")
val IntOffset.Companion.VectorConverter: TwoWayConverter<IntOffset, AnimationVector2D>
get() = IntOffsetToVector
/**
* A type converter that converts a [IntSize] to a [AnimationVector2D], and vice versa.
*/
+@Deprecated("IntSize.VectorConverter has been moved to animation-core library")
val IntSize.Companion.VectorConverter: TwoWayConverter<IntSize, AnimationVector2D>
get() = IntSizeToVector
\ No newline at end of file
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SingleValueAnimation.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SingleValueAnimation.kt
index 5fd5dab..84ae2d8 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SingleValueAnimation.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/SingleValueAnimation.kt
@@ -21,10 +21,10 @@
import androidx.compose.animation.core.AnimationState
import androidx.compose.animation.core.AnimationVector
import androidx.compose.animation.core.AnimationVector1D
-import androidx.compose.animation.core.AnimationVector4D
import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.TwoWayConverter
import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.VisibilityThreshold
import androidx.compose.animation.core.animateTo
import androidx.compose.animation.core.isFinished
import androidx.compose.runtime.Composable
@@ -45,26 +45,6 @@
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.Position
-import androidx.compose.ui.unit.dp
-
-internal const val DpVisibilityThreshold = 0.1f
-internal const val PxVisibilityThreshold = 0.5f
-
-// Dp-based visibility threshold
-private val DpVisibilityThreshold4D = AnimationVector4D(
- DpVisibilityThreshold,
- DpVisibilityThreshold,
- DpVisibilityThreshold,
- DpVisibilityThreshold
-)
-
-// Px-based visibility threshold
-private val PxVisibilityThreshold4D = AnimationVector4D(
- PxVisibilityThreshold,
- PxVisibilityThreshold,
- PxVisibilityThreshold,
- PxVisibilityThreshold
-)
private val defaultAnimation = SpringSpec<Float>()
@@ -159,7 +139,7 @@
fun animate(
target: Dp,
animSpec: AnimationSpec<Dp> = remember {
- SpringSpec(visibilityThreshold = DpVisibilityThreshold.dp)
+ SpringSpec(visibilityThreshold = Dp.VisibilityThreshold)
},
endListener: ((Dp) -> Unit)? = null
): Dp {
@@ -187,7 +167,7 @@
target: Position,
animSpec: AnimationSpec<Position> = remember {
SpringSpec(
- visibilityThreshold = Position(DpVisibilityThreshold.dp, DpVisibilityThreshold.dp)
+ visibilityThreshold = Position.VisibilityThreshold
)
},
endListener: ((Position) -> Unit)? = null
@@ -217,7 +197,7 @@
fun animate(
target: Size,
animSpec: AnimationSpec<Size> = remember {
- SpringSpec(visibilityThreshold = Size(PxVisibilityThreshold, PxVisibilityThreshold))
+ SpringSpec(visibilityThreshold = Size.VisibilityThreshold)
},
endListener: ((Size) -> Unit)? = null
): Size {
@@ -244,10 +224,7 @@
fun animate(
target: Bounds,
animSpec: AnimationSpec<Bounds> = remember {
- SpringSpec(
- visibilityThreshold = Bounds.VectorConverter.convertFromVector
- (DpVisibilityThreshold4D)
- )
+ SpringSpec(visibilityThreshold = Bounds.VisibilityThreshold)
},
endListener: ((Bounds) -> Unit)? = null
): Bounds {
@@ -278,7 +255,7 @@
fun animate(
target: Offset,
animSpec: AnimationSpec<Offset> = remember {
- SpringSpec(visibilityThreshold = Offset(PxVisibilityThreshold, PxVisibilityThreshold))
+ SpringSpec(visibilityThreshold = Offset.VisibilityThreshold)
},
endListener: ((Offset) -> Unit)? = null
): Offset {
@@ -307,10 +284,7 @@
fun animate(
target: Rect,
animSpec: AnimationSpec<Rect> = remember {
- SpringSpec(
- visibilityThreshold =
- Rect.VectorConverter.convertFromVector(PxVisibilityThreshold4D)
- )
+ SpringSpec(visibilityThreshold = Rect.VisibilityThreshold)
},
endListener: ((Rect) -> Unit)? = null
): Rect {
@@ -364,7 +338,7 @@
fun animate(
target: IntOffset,
animSpec: AnimationSpec<IntOffset> = remember {
- SpringSpec(visibilityThreshold = IntOffset(1, 1))
+ SpringSpec(visibilityThreshold = IntOffset.VisibilityThreshold)
},
endListener: ((IntOffset) -> Unit)? = null
): IntOffset {
@@ -390,7 +364,7 @@
fun animate(
target: IntSize,
animSpec: AnimationSpec<IntSize> = remember {
- SpringSpec(visibilityThreshold = IntSize(1, 1))
+ SpringSpec(visibilityThreshold = IntSize.VisibilityThreshold)
},
endListener: ((IntSize) -> Unit)? = null
): IntSize {
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/Transition.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/Transition.kt
index cb8442f..7ae0bb2 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/Transition.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/Transition.kt
@@ -18,18 +18,28 @@
import androidx.compose.animation.core.AnimationClockObservable
import androidx.compose.animation.core.AnimationVector
+import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.InternalAnimationApi
import androidx.compose.animation.core.PropKey
+import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.TransitionAnimation
import androidx.compose.animation.core.TransitionDefinition
import androidx.compose.animation.core.TransitionState
+import androidx.compose.animation.core.animateValue
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.keyframes
+import androidx.compose.animation.core.repeatable
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.onCommit
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.AmbientAnimationClock
import androidx.compose.ui.util.annotation.VisibleForTesting
@@ -130,6 +140,7 @@
) : TransitionState {
private var animationPulse by mutableStateOf(0L)
+
@InternalAnimationApi
val anim: TransitionAnimation<T> =
TransitionAnimation(transitionDef, clock, initState, label).apply {
@@ -144,4 +155,42 @@
val pulse = animationPulse
return anim[propKey]
}
-}
\ No newline at end of file
+}
+
+/**
+ * Creates a [Color] animation as a part of the given [Transition]. This means the lifecycle
+ * of this animation will be managed by the [Transition].
+ *
+ * [targetValueByState] is used as a mapping from a target state to the target value of this
+ * animation. [Transition] will be using this mapping to determine what value to target this
+ * animation towards. __Note__ that [targetValueByState] is a composable function. This means the
+ * mapping function could access states, ambient, themes, etc. If the targetValue changes outside
+ * of a [Transition] run (i.e. when the [Transition] already reached its targetState), the
+ * [Transition] will start running again to ensure this animation reaches its new target smoothly.
+ *
+ * An optional [transitionSpec] can be provided to specify (potentially different) animation for
+ * each pair of initialState and targetState. [FiniteAnimationSpec] includes any non-infinite
+ * animation, such as [tween], [spring], [keyframes] and even [repeatable], but not
+ * [infiniteRepeatable]. By default, [transitionSpec] uses a [spring] animation for all transition
+ * destinations.
+ *
+ * @return A [State] object, the value of which is updated by animation
+ *
+ * @see animateValue
+ * @see androidx.compose.animation.core.animateFloat
+ * @see androidx.compose.animation.core.Transition
+ * @see androidx.compose.animation.core.updateTransition
+ */
+@Composable
+inline fun <S> Transition<S>.animateColor(
+ noinline transitionSpec:
+ @Composable (states: Transition.States<S>) -> FiniteAnimationSpec<Color> = { spring() },
+ targetValueByState: @Composable (state: S) -> Color
+): State<Color> {
+ val colorSpace = targetValueByState(targetState).colorSpace
+ val typeConverter = remember(colorSpace) {
+ Color.VectorConverter(colorSpace)
+ }
+
+ return animateValue(typeConverter, transitionSpec, targetValueByState)
+}
diff --git a/compose/animation/animation/src/test/kotlin/androidx/compose/animation/ConverterTest.kt b/compose/animation/animation/src/test/kotlin/androidx/compose/animation/ConverterTest.kt
index cf2131d..49a60a3 100644
--- a/compose/animation/animation/src/test/kotlin/androidx/compose/animation/ConverterTest.kt
+++ b/compose/animation/animation/src/test/kotlin/androidx/compose/animation/ConverterTest.kt
@@ -19,6 +19,7 @@
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.AnimationVector2D
import androidx.compose.animation.core.AnimationVector4D
+import androidx.compose.animation.core.VectorConverter
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
diff --git a/compose/compiler/compiler-hosted/integration-tests/lint-baseline.xml b/compose/compiler/compiler-hosted/integration-tests/lint-baseline.xml
deleted file mode 100644
index db82975..0000000
--- a/compose/compiler/compiler-hosted/integration-tests/lint-baseline.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-alpha15" client="gradle" variant="debug" version="4.2.0-alpha15">
-
- <issue
- id="IgnoreWithoutReason"
- message="Test is ignored without giving any explanation"
- errorLine1=" @Ignore"
- errorLine2=" ~~~~~~~">
- <location
- file="src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralCodegenTests.kt"
- line="41"
- column="5"/>
- </issue>
-
- <issue
- id="IgnoreWithoutReason"
- message="Test is ignored without giving any explanation"
- errorLine1=" @Ignore"
- errorLine2=" ~~~~~~~">
- <location
- file="src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralCodegenTests.kt"
- line="63"
- column="5"/>
- </issue>
-
-</issues>
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
index de2790c..6dcaf71 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
@@ -68,11 +68,10 @@
abstract class ComposeIrTransformTest : AbstractIrTransformTest() {
open val liveLiteralsEnabled get() = false
open val sourceInformationEnabled get() = true
- open val intrinsicRememberEnabled get() = false
private val extension = ComposeIrGenerationExtension(
liveLiteralsEnabled,
sourceInformationEnabled,
- intrinsicRememberEnabled
+ intrinsicRememberEnabled = true
)
// Some tests require the plugin context in order to perform assertions, for example, a
// context is required to determine the stability of a type using the StabilityInferencer.
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractLoweringTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractLoweringTests.kt
index 0cd4d99..b0eed19 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractLoweringTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractLoweringTests.kt
@@ -16,7 +16,6 @@
package androidx.compose.compiler.plugins.kotlin
-import android.view.View
import androidx.compose.runtime.Composer
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.snapshots.Snapshot
@@ -42,9 +41,6 @@
)
}
- @Suppress("UNCHECKED_CAST")
- fun View.getComposedSet(tagId: Int): Set<String>? = getTag(tagId) as? Set<String>
-
@OptIn(ExperimentalComposeApi::class)
protected fun execute(block: () -> Unit) {
val scheduler = RuntimeEnvironment.getMasterScheduler()
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/CodegenMetadataTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/CodegenMetadataTests.kt
new file mode 100644
index 0000000..bacb9b6
--- /dev/null
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/CodegenMetadataTests.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.compose.compiler.plugins.kotlin
+
+import org.jetbrains.kotlin.config.CompilerConfiguration
+import org.junit.Test
+
+class CodegenMetadataTests : AbstractLoweringTests() {
+
+ override fun updateConfiguration(configuration: CompilerConfiguration) {
+ super.updateConfiguration(configuration)
+ configuration.put(ComposeConfiguration.LIVE_LITERALS_ENABLED_KEY, true)
+ }
+
+ @Test
+ fun testBasicFunctionality(): Unit = ensureSetup {
+ val className = "Test_${uniqueNumber++}"
+ val fileName = "$className.kt"
+ val loader = classLoader(
+ """
+ import kotlin.reflect.full.primaryConstructor
+ import kotlin.reflect.jvm.isAccessible
+ data class MyClass(val someBoolean: Boolean? = false)
+ object Main { @JvmStatic fun main() { MyClass::class.java.kotlin.primaryConstructor!!.isAccessible = true } }
+ """,
+ fileName,
+ true
+ )
+ val main = loader.loadClass("Main").methods.single { it.name == "main" }
+ main.invoke(null)
+ }
+}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallLoweringTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallLoweringTests.kt
index bf3a820..9a43019 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallLoweringTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallLoweringTests.kt
@@ -16,6 +16,7 @@
package androidx.compose.compiler.plugins.kotlin
+import android.view.View
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
@@ -179,13 +180,13 @@
"""
import androidx.compose.runtime.*
- @Composable val foo get() = 123
+ val foo @Composable get() = 123
class A {
- @Composable val bar get() = 123
+ val bar @Composable get() = 123
}
- @Composable val A.bam get() = 123
+ val A.bam @Composable get() = 123
@Composable fun Foo() {
}
@@ -260,13 +261,13 @@
fun testPropertyValues(): Unit = ensureSetup {
compose(
"""
- @Composable val foo get() = "123"
+ val foo @Composable get() = "123"
class A {
- @Composable val bar get() = "123"
+ val bar @Composable get() = "123"
}
- @Composable val A.bam get() = "123"
+ val A.bam @Composable get() = "123"
@Composable
fun App() {
@@ -2728,6 +2729,9 @@
}
}
+@Suppress("UNCHECKED_CAST")
+fun View.getComposedSet(tagId: Int): Set<String>? = getTag(tagId) as? Set<String>
+
private val noParameters = { emptyMap<String, String>() }
private inline fun <reified T : PsiElement> PsiElement.parentOfType(): T? = parentOfType(T::class)
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallResolverTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallResolverTests.kt
index e6b6625..ba815cf 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallResolverTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallResolverTests.kt
@@ -34,13 +34,13 @@
"""
import androidx.compose.runtime.*
- @Composable val foo get() = 123
+ val foo @Composable get() = 123
class A {
- @Composable val bar get() = 123
+ val bar @Composable get() = 123
}
- @Composable val A.bam get() = 123
+ val A.bam @Composable get() = 123
@Composable
fun test() {
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 ef97c5e..fa1abfc 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
@@ -855,12 +855,11 @@
@Test
fun testComposableTopLevelProperty(): Unit = checkApi(
"""
- @Composable val foo: Int get() { return 123 }
+ val foo: Int @Composable get() { return 123 }
""",
"""
public final class TestKt {
public final static getFoo(Landroidx/compose/runtime/Composer;I)I
- public static synthetic getFoo%annotations()V
}
"""
)
@@ -869,14 +868,13 @@
fun testComposableProperty(): Unit = checkApi(
"""
class Foo {
- @Composable val foo: Int get() { return 123 }
+ val foo: Int @Composable get() { return 123 }
}
""",
"""
public final class Foo {
public <init>()V
public final getFoo(Landroidx/compose/runtime/Composer;I)I
- public static synthetic getFoo%annotations()V
public final static I %stable
static <clinit>()V
}
@@ -942,7 +940,7 @@
@Test
fun testCallingProperties(): Unit = checkApi(
"""
- @Composable val bar: Int get() { return 123 }
+ val bar: Int @Composable get() { return 123 }
@Composable fun Example() {
bar
@@ -951,7 +949,6 @@
"""
public final class TestKt {
public final static getBar(Landroidx/compose/runtime/Composer;I)I
- public static synthetic getBar%annotations()V
final static INNERCLASS TestKt%Example%1 null null
public final static Example(Landroidx/compose/runtime/Composer;I)V
}
@@ -1201,8 +1198,7 @@
@Test
fun testDexNaming(): Unit = checkApi(
"""
- @Composable
- val myProperty: () -> Unit get() {
+ val myProperty: () -> Unit @Composable get() {
return { }
}
""",
@@ -1210,7 +1206,6 @@
public final class TestKt {
final static INNERCLASS TestKt%myProperty%1 null null
public final static getMyProperty(Landroidx/compose/runtime/Composer;I)Lkotlin/jvm/functions/Function0;
- public static synthetic getMyProperty%annotations()V
}
final class TestKt%myProperty%1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function0 {
<init>()V
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
index 07fa0354..6228e46 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
@@ -55,7 +55,7 @@
@Test
fun testCallingProperties(): Unit = composerParam(
"""
- @Composable val bar: Int get() { return 123 }
+ val bar: Int @Composable get() { return 123 }
@ComposableContract(restartable = false) @Composable fun Example() {
bar
@@ -289,8 +289,7 @@
@Test
fun testDexNaming(): Unit = composerParam(
"""
- @Composable
- val myProperty: () -> Unit get() {
+ val myProperty: () -> Unit @Composable get() {
return { }
}
""",
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt
index 30e6dbf..93459df 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt
@@ -447,7 +447,7 @@
import androidx.compose.runtime.Composable
class Foo {
- @Composable val value: Int get() = 123
+ val value: Int @Composable get() = 123
}
"""
),
@@ -687,7 +687,7 @@
import androidx.compose.runtime.*
- @Composable val foo: Int get() { return 123 }
+ val foo: Int @Composable get() { return 123 }
"""
),
"Main" to mapOf(
@@ -740,6 +740,35 @@
}
@Test
+ fun testXModuleComposableProperty(): Unit = ensureSetup {
+ compile(
+ mapOf(
+ "library module" to mapOf(
+ "a/Foo.kt" to """
+ package a
+
+ import androidx.compose.runtime.*
+
+ val foo: () -> Unit
+ @Composable get() = {}
+ """
+ ),
+ "Main" to mapOf(
+ "B.kt" to """
+ import a.foo
+ import androidx.compose.runtime.*
+
+ @Composable fun Example() {
+ val bar = foo
+ bar()
+ }
+ """
+ )
+ )
+ )
+ }
+
+ @Test
fun testXModuleCtorComposableParam(): Unit = ensureSetup {
compile(
mapOf(
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralCodegenTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralCodegenTests.kt
index 800bd34..9e9ab11 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralCodegenTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralCodegenTests.kt
@@ -38,7 +38,7 @@
configuration.put(ComposeConfiguration.LIVE_LITERALS_ENABLED_KEY, true)
}
- @Ignore
+ @Ignore("Live literals are currently disabled by default")
@Test
fun testBasicFunctionality(): Unit = ensureSetup {
compose(
@@ -60,7 +60,7 @@
}
}
- @Ignore
+ @Ignore("Live literals are currently disabled by default")
@Test
fun testObjectFieldsLoweredToStaticFields(): Unit = ensureSetup {
validateBytecode(
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
index 524d05e..161a44a 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
@@ -19,7 +19,6 @@
import org.junit.Test
class RememberIntrinsicTransformTests : ComposeIrTransformTest() {
- override val intrinsicRememberEnabled: Boolean get() = true
private fun comparisonPropagation(
unchecked: String,
checked: String,
@@ -42,6 +41,214 @@
)
@Test
+ fun testElidedRememberInsideIfDeoptsRememberAfterIf(): Unit = comparisonPropagation(
+ "",
+ """
+ import androidx.compose.runtime.ComposableContract
+
+ @Composable
+ @ComposableContract(restartable = false)
+ fun app(x: Boolean) {
+ val a = if (x) { remember { 1 } } else { 2 }
+ val b = remember { 2 }
+ }
+ """,
+ """
+ @Composable
+ @ComposableContract(restartable = false)
+ fun app(x: Boolean, %composer: Composer<*>?, %changed: Int) {
+ %composer.startReplaceableGroup(<>, "C(app)<rememb...>:Test.kt")
+ val a = if (x) {
+ %composer.startReplaceableGroup(<>)
+ val tmp0_group = %composer.cache(false) {
+ val tmp0_return = 1
+ tmp0_return
+ }
+ %composer.endReplaceableGroup()
+ tmp0_group
+ } else {
+ %composer.startReplaceableGroup(<>)
+ %composer.endReplaceableGroup()
+ 2
+ }
+ val b = remember({
+ val tmp0_return = 2
+ tmp0_return
+ }, %composer, 0)
+ %composer.endReplaceableGroup()
+ }
+ """
+ )
+
+ @Test
+ fun testMultipleParamInputs(): Unit = comparisonPropagation(
+ """
+ """,
+ """
+ @Composable
+ fun <T> loadResourceInternal(
+ key: String,
+ pendingResource: T? = null,
+ failedResource: T? = null
+ ): Boolean {
+ val deferred = remember(key, pendingResource, failedResource) {
+ 123
+ }
+ return deferred > 10
+ }
+ """,
+ """
+ @Composable
+ fun <T> loadResourceInternal(key: String, pendingResource: T?, failedResource: T?, %composer: Composer<*>?, %changed: Int, %default: Int): Boolean {
+ %composer.startReplaceableGroup(<>, "C(loadResourceInternal)P(1,2):Test.kt")
+ val pendingResource = if (%default and 0b0010 !== 0) null else pendingResource
+ val failedResource = if (%default and 0b0100 !== 0) null else failedResource
+ val deferred = %composer.cache(%changed and 0b1110 xor 0b0110 > 4 && %composer.changed(key) || %changed and 0b0110 === 0b0100 or %changed and 0b01110000 xor 0b00110000 > 32 && %composer.changed(pendingResource) || %changed and 0b00110000 === 0b00100000 or %changed and 0b001110000000 xor 0b000110000000 > 256 && %composer.changed(failedResource) || %changed and 0b000110000000 === 0b000100000000) {
+ val tmp0_return = 123
+ tmp0_return
+ }
+ val tmp0 = deferred > 10
+ %composer.endReplaceableGroup()
+ return tmp0
+ }
+ """
+ )
+
+ @Test
+ fun testRestartableParameterInputsStableUnstableUncertain(): Unit = comparisonPropagation(
+ """
+ class KnownStable
+ class KnownUnstable(var x: Int)
+ interface Uncertain
+ """,
+ """
+ @Composable
+ fun test1(x: KnownStable) {
+ remember(x) { 1 }
+ }
+ @Composable
+ fun test2(x: KnownUnstable) {
+ remember(x) { 1 }
+ }
+ @Composable
+ fun test3(x: Uncertain) {
+ remember(x) { 1 }
+ }
+ """,
+ """
+ @Composable
+ fun test1(x: KnownStable, %composer: Composer<*>?, %changed: Int) {
+ %composer.startRestartGroup(<>, "C(test1):Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ %composer.cache(%dirty and 0b1110 === 0b0100) {
+ val tmp0_return = 1
+ tmp0_return
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer<*>?, %force: Int ->
+ test1(x, %composer, %changed or 0b0001)
+ }
+ }
+ @Composable
+ fun test2(x: KnownUnstable, %composer: Composer<*>?, %changed: Int) {
+ %composer.startRestartGroup(<>, "C(test2):Test.kt")
+ %composer.cache(%composer.changed(x)) {
+ val tmp0_return = 1
+ tmp0_return
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer<*>?, %force: Int ->
+ test2(x, %composer, %changed or 0b0001)
+ }
+ }
+ @Composable
+ fun test3(x: Uncertain, %composer: Composer<*>?, %changed: Int) {
+ %composer.startRestartGroup(<>, "C(test3):Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ %composer.cache(%dirty and 0b1110 === 0b0100 || %dirty and 0b1000 !== 0 && %composer.changed(x)) {
+ val tmp0_return = 1
+ tmp0_return
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer<*>?, %force: Int ->
+ test3(x, %composer, %changed or 0b0001)
+ }
+ }
+ """
+ )
+
+ @Test
+ fun testNonRestartableParameterInputsStableUnstableUncertain(): Unit = comparisonPropagation(
+ """
+ class KnownStable
+ class KnownUnstable(var x: Int)
+ interface Uncertain
+ """,
+ """
+ import androidx.compose.runtime.ComposableContract
+
+ @Composable
+ @ComposableContract(restartable=false)
+ fun test1(x: KnownStable) {
+ remember(x) { 1 }
+ }
+ @Composable
+ @ComposableContract(restartable=false)
+ fun test2(x: KnownUnstable) {
+ remember(x) { 1 }
+ }
+ @Composable
+ @ComposableContract(restartable=false)
+ fun test3(x: Uncertain) {
+ remember(x) { 1 }
+ }
+ """,
+ """
+ @Composable
+ @ComposableContract(restartable = false)
+ fun test1(x: KnownStable, %composer: Composer<*>?, %changed: Int) {
+ %composer.startReplaceableGroup(<>, "C(test1):Test.kt")
+ %composer.cache(%changed and 0b1110 xor 0b0110 > 4 && %composer.changed(x) || %changed and 0b0110 === 0b0100) {
+ val tmp0_return = 1
+ tmp0_return
+ }
+ %composer.endReplaceableGroup()
+ }
+ @Composable
+ @ComposableContract(restartable = false)
+ fun test2(x: KnownUnstable, %composer: Composer<*>?, %changed: Int) {
+ %composer.startReplaceableGroup(<>, "C(test2):Test.kt")
+ %composer.cache(%composer.changed(x)) {
+ val tmp0_return = 1
+ tmp0_return
+ }
+ %composer.endReplaceableGroup()
+ }
+ @Composable
+ @ComposableContract(restartable = false)
+ fun test3(x: Uncertain, %composer: Composer<*>?, %changed: Int) {
+ %composer.startReplaceableGroup(<>, "C(test3):Test.kt")
+ %composer.cache(%changed and 0b1110 xor 0b0110 > 4 && %composer.changed(x) || %changed and 0b0110 === 0b0100) {
+ val tmp0_return = 1
+ tmp0_return
+ }
+ %composer.endReplaceableGroup()
+ }
+ """
+ )
+
+ @Test
fun testPassedArgs(): Unit = comparisonPropagation(
"""
class Foo(val a: Int, val b: Int)
@@ -54,7 +261,7 @@
@Composable
fun rememberFoo(a: Int, b: Int, %composer: Composer<*>?, %changed: Int): Foo {
%composer.startReplaceableGroup(<>, "C(rememberFoo):Test.kt")
- val tmp0 = %composer.cache(%changed and 0b0110 === 0 && %composer.changed(a) || %changed and 0b1110 === 0b0100 or %changed and 0b00110000 === 0 && %composer.changed(b) || %changed and 0b01110000 === 0b00100000) {
+ val tmp0 = %composer.cache(%changed and 0b1110 xor 0b0110 > 4 && %composer.changed(a) || %changed and 0b0110 === 0b0100 or %changed and 0b01110000 xor 0b00110000 > 32 && %composer.changed(b) || %changed and 0b00110000 === 0b00100000) {
val tmp0_return = Foo(a, b)
tmp0_return
}
@@ -676,7 +883,7 @@
fun Test(a: Int, %composer: Composer<*>?, %changed: Int): Foo {
%composer.startReplaceableGroup(<>, "C(Test):Test.kt")
val b = someInt()
- val tmp0 = %composer.cache(%changed and 0b0110 === 0 && %composer.changed(a) || %changed and 0b1110 === 0b0100 or %composer.changed(b)) {
+ val tmp0 = %composer.cache(%changed and 0b1110 xor 0b0110 > 4 && %composer.changed(a) || %changed and 0b0110 === 0b0100 or %composer.changed(b)) {
val tmp0_return = Foo(a, b)
tmp0_return
}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableCheckerTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableCheckerTests.kt
index b05df3c..3b124e8 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableCheckerTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableCheckerTests.kt
@@ -140,7 +140,7 @@
"""
import androidx.compose.runtime.*
@Composable fun C(): Int { return 123 }
- @Composable val cProp: Int get() = C()
+ val cProp: Int @Composable get() = C()
"""
)
@@ -157,7 +157,7 @@
import androidx.compose.runtime.*
@Composable fun C(): Int { return 123 }
val ncProp: Int = <!COMPOSABLE_INVOCATION!>C<!>()
- @Composable val <!COMPOSABLE_PROPERTY_BACKING_FIELD!>cProp<!>: Int = <!COMPOSABLE_INVOCATION!>C<!>()
+ @Composable val <!COMPOSABLE_PROPERTY_BACKING_FIELD,DEPRECATED_COMPOSABLE_PROPERTY!>cProp<!>: Int = <!COMPOSABLE_INVOCATION!>C<!>()
"""
)
@@ -868,7 +868,7 @@
"""
import androidx.compose.runtime.*;
- @Composable val foo: Int get() = 123
+ val foo: Int @Composable get() = 123
fun <!COMPOSABLE_EXPECTED!>App<!>() {
<!COMPOSABLE_INVOCATION!>foo<!>
@@ -879,7 +879,7 @@
"""
import androidx.compose.runtime.*;
- @Composable val foo: Int get() = 123
+ val foo: Int @Composable get() = 123
@Composable
fun App() {
@@ -927,10 +927,10 @@
import androidx.compose.runtime.*;
class A {
- @Composable val bar get() = 123
+ val bar @Composable get() = 123
}
- @Composable val A.bam get() = 123
+ val A.bam @Composable get() = 123
@Composable
fun App() {
@@ -966,7 +966,7 @@
@Composable fun Foo() {}
- @Composable val bam: Int get() {
+ val bam: Int @Composable get() {
Foo()
return 123
}
@@ -1003,7 +1003,7 @@
val x = object {
val <!COMPOSABLE_EXPECTED!>a<!> get() =
<!COMPOSABLE_INVOCATION!>remember<!> { mutableStateOf(2) }
- @Composable val c get() = remember { mutableStateOf(4) }
+ val c @Composable get() = remember { mutableStateOf(4) }
@Composable fun bar() { Foo() }
fun <!COMPOSABLE_EXPECTED!>foo<!>() {
<!COMPOSABLE_INVOCATION!>Foo<!>()
@@ -1012,7 +1012,7 @@
class Bar {
val <!COMPOSABLE_EXPECTED!>b<!> get() =
<!COMPOSABLE_INVOCATION!>remember<!> { mutableStateOf(6) }
- @Composable val c get() = remember { mutableStateOf(7) }
+ val c @Composable get() = remember { mutableStateOf(7) }
}
fun <!COMPOSABLE_EXPECTED!>Bam<!>() {
<!COMPOSABLE_INVOCATION!>Foo<!>()
@@ -1036,7 +1036,7 @@
@Composable fun App() {
val x = object {
val <!COMPOSABLE_EXPECTED!>a<!> get() = <!COMPOSABLE_INVOCATION!>remember<!> { mutableStateOf(2) }
- @Composable val c get() = remember { mutableStateOf(4) }
+ val c @Composable get() = remember { mutableStateOf(4) }
fun <!COMPOSABLE_EXPECTED!>foo<!>() {
<!COMPOSABLE_INVOCATION!>Foo<!>()
}
@@ -1044,7 +1044,7 @@
}
class Bar {
val <!COMPOSABLE_EXPECTED!>b<!> get() = <!COMPOSABLE_INVOCATION!>remember<!> { mutableStateOf(6) }
- @Composable val c get() = remember { mutableStateOf(7) }
+ val c @Composable get() = remember { mutableStateOf(7) }
}
fun <!COMPOSABLE_EXPECTED!>Bam<!>() {
<!COMPOSABLE_INVOCATION!>Foo<!>()
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableDeclarationCheckerTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableDeclarationCheckerTests.kt
index c63dffc..4463850 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableDeclarationCheckerTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableDeclarationCheckerTests.kt
@@ -29,7 +29,10 @@
import androidx.compose.runtime.Composable
@Composable
- val <!COMPOSABLE_PROPERTY_BACKING_FIELD!>foo<!>: Int = 123
+ val <!DEPRECATED_COMPOSABLE_PROPERTY,COMPOSABLE_PROPERTY_BACKING_FIELD!>foo<!>: Int = 123
+
+ val <!COMPOSABLE_PROPERTY_BACKING_FIELD!>bar<!>: Int = 123
+ @Composable get() = field
"""
)
}
@@ -78,7 +81,9 @@
import androidx.compose.runtime.Composable
@Composable
- val bar: Int get() = 123
+ val <!DEPRECATED_COMPOSABLE_PROPERTY!>bar<!>: Int get() = 123
+
+ val foo: Int @Composable get() = 123
"""
)
}
@@ -89,9 +94,67 @@
import androidx.compose.runtime.Composable
@Composable
- var <!COMPOSABLE_VAR!>bam<!>: Int
+ var <!DEPRECATED_COMPOSABLE_PROPERTY, COMPOSABLE_VAR!>bam<!>: Int
get() { return 123 }
set(value) { print(value) }
+
+ var <!COMPOSABLE_VAR!>bam2<!>: Int
+ @Composable get() { return 123 }
+ set(value) { print(value) }
+
+ var <!COMPOSABLE_VAR!>bam3<!>: Int
+ @Composable get() { return 123 }
+ <!WRONG_ANNOTATION_TARGET!>@Composable<!> set(value) { print(value) }
+
+ var <!COMPOSABLE_VAR!>bam4<!>: Int
+ get() { return 123 }
+ <!WRONG_ANNOTATION_TARGET!>@Composable<!> set(value) { print(value) }
+ """
+ )
+ }
+
+ fun testPropertyGetterAllForms() {
+ doTest(
+ """
+ import androidx.compose.runtime.Composable
+
+ @Composable val <!DEPRECATED_COMPOSABLE_PROPERTY!>bar1<!>: Int get() = 123
+ val bar2: Int @Composable get() = 123
+ @get:Composable val bar3: Int get() = 123
+
+ interface Foo {
+ @Composable val <!DEPRECATED_COMPOSABLE_PROPERTY!>bar1<!>: Int get() = 123
+ val bar2: Int @Composable get() = 123
+ @get:Composable val bar3: Int get() = 123
+ }
+ """
+ )
+ }
+
+ fun testMarkedPropInOverrideMarkedGetter() {
+ doTest(
+ """
+ import androidx.compose.runtime.Composable
+ interface A {
+ val foo: Int @Composable get() = 123
+ }
+ class Impl : A {
+ @Composable override val <!DEPRECATED_COMPOSABLE_PROPERTY!>foo<!>: Int get() = 123
+ }
+ """
+ )
+ }
+
+ fun testMarkedGetterInOverrideMarkedProp() {
+ doTest(
+ """
+ import androidx.compose.runtime.Composable
+ interface A {
+ @Composable val <!DEPRECATED_COMPOSABLE_PROPERTY!>foo<!>: Int get() = 123
+ }
+ class Impl : A {
+ override val foo: Int @Composable get() = 123
+ }
"""
)
}
@@ -128,16 +191,31 @@
@Composable
fun composableFunction(param: Boolean): Boolean
@Composable
- val composableProperty: Boolean
+ val <!DEPRECATED_COMPOSABLE_PROPERTY!>composableProperty<!>: Boolean
fun nonComposableFunction(param: Boolean): Boolean
val nonComposableProperty: Boolean
}
object FakeFoo : Foo {
<!CONFLICTING_OVERLOADS!>override fun composableFunction(param: Boolean)<!> = true
- <!CONFLICTING_OVERLOADS!>override val composableProperty: Boolean<!> get() = true
+ <!CONFLICTING_OVERLOADS!>override val composableProperty: Boolean<!> <!CONFLICTING_OVERLOADS!>get()<!> = true
<!CONFLICTING_OVERLOADS!>@Composable override fun nonComposableFunction(param: Boolean)<!> = true
- <!CONFLICTING_OVERLOADS!>@Composable override val nonComposableProperty: Boolean<!> get() = true
+ <!CONFLICTING_OVERLOADS!>@Composable override val <!DEPRECATED_COMPOSABLE_PROPERTY!>nonComposableProperty<!>: Boolean<!> <!CONFLICTING_OVERLOADS!>get()<!> = true
+ }
+
+ interface Bar {
+ @Composable
+ fun composableFunction(param: Boolean): Boolean
+ val composableProperty: Boolean @Composable get()
+ fun nonComposableFunction(param: Boolean): Boolean
+ val nonComposableProperty: Boolean
+ }
+
+ object FakeBar : Bar {
+ <!CONFLICTING_OVERLOADS!>override fun composableFunction(param: Boolean)<!> = true
+ <!CONFLICTING_OVERLOADS!>override val composableProperty: Boolean<!> <!CONFLICTING_OVERLOADS!>get()<!> = true
+ <!CONFLICTING_OVERLOADS!>@Composable override fun nonComposableFunction(param: Boolean)<!> = true
+ <!CONFLICTING_OVERLOADS!>override val nonComposableProperty: Boolean<!> <!CONFLICTING_OVERLOADS!>@Composable get()<!> = true
}
"""
)
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableCallChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableCallChecker.kt
index 37784c8..6bd17a8 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableCallChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableCallChecker.kt
@@ -68,6 +68,8 @@
import org.jetbrains.kotlin.types.upperIfFlexible
import org.jetbrains.kotlin.util.OperatorNameConventions
+internal const val COMPOSABLE_PROPERTIES = true
+
open class ComposableCallChecker :
CallChecker,
AdditionalTypeChecker,
@@ -187,7 +189,11 @@
}
is KtPropertyAccessor -> {
val property = node.property
- if (!property.annotationEntries.hasComposableAnnotation(bindingContext)) {
+ val isComposable = node
+ .annotationEntries.hasComposableAnnotation(bindingContext)
+ val propertyIsComposable = property
+ .annotationEntries.hasComposableAnnotation(bindingContext)
+ if (!(isComposable || COMPOSABLE_PROPERTIES && propertyIsComposable)) {
illegalCall(context, reportOn, property.nameIdentifier ?: property)
}
return
@@ -329,19 +335,32 @@
return when (candidateDescriptor) {
is ValueParameterDescriptor -> false
is LocalVariableDescriptor -> false
- is PropertyDescriptor -> candidateDescriptor.hasComposableAnnotation()
- is PropertyGetterDescriptor ->
- candidateDescriptor.correspondingProperty.hasComposableAnnotation()
+ is PropertyDescriptor -> {
+ val isGetter = valueArguments.isEmpty()
+ val getter = candidateDescriptor.getter
+ if (isGetter && getter != null) {
+ getter.hasComposableAnnotation() ||
+ (COMPOSABLE_PROPERTIES && candidateDescriptor.hasComposableAnnotation())
+ } else {
+ false
+ }
+ }
+ is PropertyGetterDescriptor -> candidateDescriptor.hasComposableAnnotation() || (
+ COMPOSABLE_PROPERTIES && candidateDescriptor.correspondingProperty
+ .hasComposableAnnotation()
+ )
else -> candidateDescriptor.hasComposableAnnotation()
}
}
internal fun CallableDescriptor.isMarkedAsComposable(): Boolean {
return when (this) {
- is PropertyGetterDescriptor -> correspondingProperty.hasComposableAnnotation()
+ is PropertyGetterDescriptor -> hasComposableAnnotation() || (
+ COMPOSABLE_PROPERTIES && correspondingProperty.hasComposableAnnotation()
+ )
is ValueParameterDescriptor -> type.hasComposableAnnotation()
is LocalVariableDescriptor -> type.hasComposableAnnotation()
- is PropertyDescriptor -> hasComposableAnnotation()
+ is PropertyDescriptor -> COMPOSABLE_PROPERTIES && hasComposableAnnotation()
else -> hasComposableAnnotation()
}
}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableDeclarationChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableDeclarationChecker.kt
index 8e592b6..bf9bce7 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableDeclarationChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposableDeclarationChecker.kt
@@ -26,6 +26,7 @@
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
+import org.jetbrains.kotlin.descriptors.PropertyAccessorDescriptor
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor
import org.jetbrains.kotlin.platform.TargetPlatform
@@ -33,6 +34,7 @@
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.psi.KtProperty
+import org.jetbrains.kotlin.psi.KtPropertyAccessor
import org.jetbrains.kotlin.resolve.checkers.DeclarationChecker
import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext
import org.jetbrains.kotlin.types.KotlinType
@@ -53,8 +55,15 @@
context: DeclarationCheckerContext
) {
when {
- declaration is KtProperty &&
+ COMPOSABLE_PROPERTIES &&
+ declaration is KtProperty &&
descriptor is PropertyDescriptor -> checkProperty(declaration, descriptor, context)
+ declaration is KtPropertyAccessor &&
+ descriptor is PropertyAccessorDescriptor -> checkPropertyAccessor(
+ declaration,
+ descriptor,
+ context
+ )
declaration is KtFunction &&
descriptor is FunctionDescriptor -> checkFunction(declaration, descriptor, context)
}
@@ -112,9 +121,58 @@
context: DeclarationCheckerContext
) {
val hasComposableAnnotation = descriptor.hasComposableAnnotation()
+ val thisIsComposable = hasComposableAnnotation || descriptor
+ .getter
+ ?.hasComposableAnnotation() == true
if (descriptor.overriddenDescriptors.isNotEmpty()) {
val override = descriptor.overriddenDescriptors.first()
- if (override.hasComposableAnnotation() != hasComposableAnnotation) {
+ val overrideIsComposable = override.hasComposableAnnotation() ||
+ override.getter?.hasComposableAnnotation() == true
+ if (overrideIsComposable != thisIsComposable) {
+ context.trace.report(
+ ComposeErrors.CONFLICTING_OVERLOADS.on(
+ declaration,
+ listOf(descriptor, override)
+ )
+ )
+ }
+ }
+ if (hasComposableAnnotation) {
+ context.trace.report(
+ ComposeErrors.DEPRECATED_COMPOSABLE_PROPERTY.on(
+ declaration.nameIdentifier ?: declaration
+ )
+ )
+ }
+ if (!hasComposableAnnotation) return
+ val initializer = declaration.initializer
+ val name = declaration.nameIdentifier
+ if (initializer != null && name != null) {
+ context.trace.report(COMPOSABLE_PROPERTY_BACKING_FIELD.on(name))
+ }
+ if (descriptor.isVar && name != null) {
+ context.trace.report(COMPOSABLE_VAR.on(name))
+ }
+ }
+
+ private fun checkPropertyAccessor(
+ declaration: KtPropertyAccessor,
+ descriptor: PropertyAccessorDescriptor,
+ context: DeclarationCheckerContext
+ ) {
+ val propertyDescriptor = descriptor.correspondingProperty
+ val propertyPsi = declaration.parent as? KtProperty ?: return
+ val name = propertyPsi.nameIdentifier
+ val initializer = propertyPsi.initializer
+ val hasComposableAnnotation = descriptor.hasComposableAnnotation()
+ val propertyHasComposableAnnotation = COMPOSABLE_PROPERTIES && propertyDescriptor
+ .hasComposableAnnotation()
+ val thisComposable = hasComposableAnnotation || propertyHasComposableAnnotation
+ if (descriptor.overriddenDescriptors.isNotEmpty()) {
+ val override = descriptor.overriddenDescriptors.first()
+ val overrideComposable = override.hasComposableAnnotation() || override
+ .correspondingProperty.hasComposableAnnotation()
+ if (overrideComposable != thisComposable) {
context.trace.report(
ComposeErrors.CONFLICTING_OVERLOADS.on(
declaration,
@@ -124,12 +182,10 @@
}
}
if (!hasComposableAnnotation) return
- val initializer = declaration.initializer
- val name = declaration.nameIdentifier
if (initializer != null && name != null) {
context.trace.report(COMPOSABLE_PROPERTY_BACKING_FIELD.on(name))
}
- if (descriptor.isVar && name != null) {
+ if (propertyDescriptor.isVar && name != null) {
context.trace.report(COMPOSABLE_VAR.on(name))
}
}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrorMessages.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrorMessages.kt
index 2d62be3..e0e887a 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrorMessages.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrorMessages.kt
@@ -90,5 +90,10 @@
RENDER_TYPE_WITH_ANNOTATIONS,
RENDER_TYPE_WITH_ANNOTATIONS
)
+ MAP.put(
+ ComposeErrors.DEPRECATED_COMPOSABLE_PROPERTY,
+ "@Composable properties should be declared with the @Composable annotation " +
+ "on the getter, and not the property itself."
+ )
}
}
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrors.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrors.kt
index b416082..8ef2b83 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrors.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeErrors.kt
@@ -85,6 +85,10 @@
)
@JvmField
+ var DEPRECATED_COMPOSABLE_PROPERTY: DiagnosticFactory0<PsiElement> =
+ DiagnosticFactory0.create(Severity.WARNING)
+
+ @JvmField
val ILLEGAL_ASSIGN_TO_UNIONTYPE =
DiagnosticFactory2.create<KtExpression, Collection<KotlinType>, Collection<KotlinType>>(
Severity.ERROR
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
index bfb6031..1ad22c2 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
@@ -37,7 +37,7 @@
class ComposeIrGenerationExtension(
@Suppress("unused") private val liveLiteralsEnabled: Boolean = false,
private val sourceInformationEnabled: Boolean = true,
- private val intrinsicRememberEnabled: Boolean = false,
+ private val intrinsicRememberEnabled: Boolean = true,
) : IrGenerationExtension {
@OptIn(ObsoleteDescriptorBasedAPI::class)
override fun generate(
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
index b55b131..98ae3f3 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
@@ -27,7 +27,10 @@
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor
import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
+import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
+import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
import org.jetbrains.kotlin.config.CompilerConfigurationKey
+import org.jetbrains.kotlin.config.KotlinCompilerVersion
import org.jetbrains.kotlin.extensions.internal.TypeResolutionInterceptor
import org.jetbrains.kotlin.serialization.DescriptorSerializer
@@ -38,6 +41,8 @@
CompilerConfigurationKey<Boolean>("Include source information in generated code")
val INTRINSIC_REMEMBER_OPTIMIZATION_ENABLED_KEY =
CompilerConfigurationKey<Boolean>("Enable optimization to treat remember as an intrinsic")
+ val SUPPRESS_KOTLIN_VERSION_COMPATIBILITY_CHECK =
+ CompilerConfigurationKey<Boolean>("Suppress Kotlin version compatibility check")
}
class ComposeCommandLineProcessor : CommandLineProcessor {
@@ -64,13 +69,21 @@
required = false,
allowMultipleOccurrences = false
)
+ val SUPPRESS_KOTLIN_VERSION_CHECK_ENABLED_OPTION = CliOption(
+ "suppressKotlinVersionCompatibilityCheck",
+ "<true|false>",
+ "Suppress Kotlin version compatibility check",
+ required = false,
+ allowMultipleOccurrences = false
+ )
}
override val pluginId = PLUGIN_ID
override val pluginOptions = listOf(
LIVE_LITERALS_ENABLED_OPTION,
SOURCE_INFORMATION_ENABLED_OPTION,
- INTRINSIC_REMEMBER_OPTIMIZATION_ENABLED_OPTION
+ INTRINSIC_REMEMBER_OPTIMIZATION_ENABLED_OPTION,
+ SUPPRESS_KOTLIN_VERSION_CHECK_ENABLED_OPTION
)
override fun processOption(
@@ -88,6 +101,10 @@
)
INTRINSIC_REMEMBER_OPTIMIZATION_ENABLED_OPTION -> configuration.put(
ComposeConfiguration.INTRINSIC_REMEMBER_OPTIMIZATION_ENABLED_KEY,
+ value != "false"
+ )
+ SUPPRESS_KOTLIN_VERSION_CHECK_ENABLED_OPTION -> configuration.put(
+ ComposeConfiguration.SUPPRESS_KOTLIN_VERSION_COMPATIBILITY_CHECK,
value == "true"
)
else -> throw CliOptionProcessingException("Unknown option: ${option.optionName}")
@@ -112,6 +129,26 @@
project: Project,
configuration: CompilerConfiguration
) {
+ val KOTLIN_VERSION_EXPECTATION = "1.4.20"
+ KotlinCompilerVersion.getVersion()?.let { version ->
+ val suppressKotlinVersionCheck = configuration.get(
+ ComposeConfiguration.SUPPRESS_KOTLIN_VERSION_COMPATIBILITY_CHECK,
+ false
+ )
+ if (!suppressKotlinVersionCheck && version != KOTLIN_VERSION_EXPECTATION) {
+ val msgCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY)
+ msgCollector?.report(
+ CompilerMessageSeverity.ERROR,
+ "This version (${VersionChecker.compilerVersion}) of the Compose" +
+ " Compiler requires Kotlin version $KOTLIN_VERSION_EXPECTATION but" +
+ " you appear to be using Kotlin version $version which is not known" +
+ " to be compatible. Please fix your configuration (or" +
+ " `suppressKotlinVersionCompatibilityCheck` but don't say I didn't" +
+ " warn you!)."
+ )
+ }
+ }
+
val liveLiteralsEnabled = configuration.get(
ComposeConfiguration.LIVE_LITERALS_ENABLED_KEY,
false
@@ -122,7 +159,7 @@
)
val intrinsicRememberEnabled = configuration.get(
ComposeConfiguration.INTRINSIC_REMEMBER_OPTIMIZATION_ENABLED_KEY,
- false
+ true
)
StorageComponentContainerContributor.registerExtension(
project,
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index b12654e..2d8e6da 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -23,31 +23,33 @@
class VersionChecker(val context: IrPluginContext) {
- /**
- * A table of version ints to version strings. This should be updated every time
- * ComposeVersion.kt is updated.
- */
- private val versionTable = mapOf(
- 1600 to "0.1.0-dev16",
- 1700 to "1.0.0-alpha06",
- 1800 to "1.0.0-alpha07",
- 1900 to "1.0.0-alpha08",
- 2000 to "1.0.0-alpha09"
- )
+ companion object {
+ /**
+ * A table of version ints to version strings. This should be updated every time
+ * ComposeVersion.kt is updated.
+ */
+ private val versionTable = mapOf(
+ 1600 to "0.1.0-dev16",
+ 1700 to "1.0.0-alpha06",
+ 1800 to "1.0.0-alpha07",
+ 1900 to "1.0.0-alpha08",
+ 2000 to "1.0.0-alpha09"
+ )
- /**
- * The minimum version int that this compiler is guaranteed to be compatible with. Typically
- * this will match the version int that is in ComposeVersion.kt in the runtime.
- */
- private val minimumRuntimeVersionInt: Int = 2000
+ /**
+ * The minimum version int that this compiler is guaranteed to be compatible with. Typically
+ * this will match the version int that is in ComposeVersion.kt in the runtime.
+ */
+ private val minimumRuntimeVersionInt: Int = 2000
- /**
- * The maven version string of this compiler. This string should be updated before/after every
- * release.
- */
- private val compilerVersion: String = "1.0.0-alpha09"
- private val minimumRuntimeVersion: String
- get() = versionTable[minimumRuntimeVersionInt] ?: "unknown"
+ /**
+ * The maven version string of this compiler. This string should be updated before/after every
+ * release.
+ */
+ val compilerVersion: String = "1.0.0-alpha09"
+ private val minimumRuntimeVersion: String
+ get() = versionTable[minimumRuntimeVersionInt] ?: "unknown"
+ }
fun check() {
val versionClass = context.referenceClass(ComposeFqNames.ComposeVersion)
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
index 1e6f5fe..d852691 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
@@ -117,6 +117,7 @@
import org.jetbrains.kotlin.ir.types.IrSimpleType
import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.ir.types.classOrNull
+import org.jetbrains.kotlin.ir.types.classifierOrFail
import org.jetbrains.kotlin.ir.types.impl.IrSimpleTypeImpl
import org.jetbrains.kotlin.ir.types.impl.IrStarProjectionImpl
import org.jetbrains.kotlin.ir.types.isNullable
@@ -720,6 +721,19 @@
)
}
+ protected fun irGreater(lhs: IrExpression, rhs: IrExpression): IrCallImpl {
+ val int = context.irBuiltIns.intType
+ val gt = context.irBuiltIns.greaterFunByOperandType[int.classifierOrFail]
+ return irCall(
+ gt!!,
+ IrStatementOrigin.GT,
+ null,
+ null,
+ lhs,
+ rhs
+ )
+ }
+
protected fun irReturn(
target: IrReturnTargetSymbol,
value: IrExpression,
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
index f38d76b..6925da4 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
@@ -252,6 +252,7 @@
interface IrChangedBitMaskValue {
fun irLowBit(): IrExpression
fun irIsolateBitsAtSlot(slot: Int, includeStableBit: Boolean): IrExpression
+ fun irSlotAnd(slot: Int, bits: Int): IrExpression
fun irHasDifferences(): IrExpression
fun irCopyToTemporary(
nameHint: String? = null,
@@ -2163,16 +2164,21 @@
private fun encounteredComposableCall(withGroups: Boolean) {
var scope: Scope? = currentScope
+ // it is important that we only report "withGroups: false" for the _nearest_ scope, and
+ // every scope above that it effectively means there was a group even if it is false
+ var groups = withGroups
loop@ while (scope != null) {
when (scope) {
is Scope.FunctionScope -> {
- scope.recordComposableCall(withGroups)
+ scope.recordComposableCall(groups)
+ groups = true
if (!scope.isInlinedLambda) {
break@loop
}
}
is Scope.BlockScope -> {
- scope.recordComposableCall(withGroups)
+ scope.recordComposableCall(groups)
+ groups = true
}
is Scope.ClassScope -> {
break@loop
@@ -2684,9 +2690,11 @@
return when {
meta.isStatic -> irConst(false)
- meta.isCertain && param is IrChangedBitMaskVariable -> {
- // if it's a dirty flag then we know that the value is now CERTAIN,
- // thus we can avoid calling changed all together
+ meta.isCertain &&
+ meta.stability.knownStable() &&
+ param is IrChangedBitMaskVariable -> {
+ // if it's a dirty flag, and the parameter is _guaranteed_ to be stable, then we
+ // know that the value is now CERTAIN, thus we can avoid calling changed completely
//
// invalid = invalid or (mask == different)
irEqual(
@@ -2694,30 +2702,55 @@
irConst(ParamState.Different.bitsForSlot(meta.maskSlot))
)
}
- meta.isCertain && param != null -> {
- // if it's a changed flag then uncertain is a possible value. If it is uncertain,
- // then we need to call changed. If it is uncertain here it will _always_ be
- // uncertain here, so this is safe. If it is not uncertain, we can just check to
- // see if its different
- // TODO(lmr): IMPORTANT QUESTION - is unstable + something other than uncertain
- // possible?
+ meta.isCertain &&
+ !meta.stability.knownUnstable() &&
+ param is IrChangedBitMaskVariable -> {
+ // if it's a dirty flag, and the parameter might be stable, then we only check
+ // changed if the value is unstable, otherwise we can just check to see if the mask
+ // is different
//
- //
- // invalid = invalid or ((mask == uncertain && changed()) || mask == different)
+ // invalid = invalid or (stable && mask == different || unstable && changed)
+
+ val maskIsStableAndDifferent = irEqual(
+ param.irIsolateBitsAtSlot(meta.maskSlot, includeStableBit = true),
+ irConst(ParamState.Different.bitsForSlot(meta.maskSlot))
+ )
+ val stableBits = param.irSlotAnd(meta.maskSlot, StabilityBits.UNSTABLE.bits)
+ val maskIsUnstableAndChanged = irAndAnd(
+ irNotEqual(stableBits, irConst(0)),
+ irChanged(arg)
+ )
+ irOrOr(
+ maskIsStableAndDifferent,
+ maskIsUnstableAndChanged
+ )
+ }
+ meta.isCertain &&
+ !meta.stability.knownUnstable() &&
+ param != null -> {
+ // if it's a changed flag then uncertain is a possible value. If it is uncertain
+ // OR unstable, then we need to call changed. If it is uncertain or unstable here
+ // it will _always_ be uncertain or unstable here, so this is safe. If it is not
+ // uncertain or unstable, we can just check to see if its different
+
+ // unstableOrUncertain = mask xor 011 > 010
+ // invalid = invalid or ((unstableOrUncertain && changed()) || mask == different)
+
+ val maskIsUnstableOrUncertain =
+ irGreater(
+ irXor(
+ param.irIsolateBitsAtSlot(meta.maskSlot, includeStableBit = true),
+ irConst(bitsForSlot(0b011, meta.maskSlot))
+ ),
+ irConst(bitsForSlot(0b010, meta.maskSlot))
+ )
irOrOr(
irAndAnd(
- irEqual(
- // we do NOT include the stable bit here because we want to capture
- // both of the cases where the type is stable and unstable.
- param.irIsolateBitsAtSlot(meta.maskSlot, includeStableBit = false),
- // NOTE: this is always "0", but i'm writing it out fully here to
- // just make the code more clear
- irConst(ParamState.Uncertain.bitsForSlot(meta.maskSlot))
- ),
+ maskIsUnstableOrUncertain,
irChanged(arg)
),
irEqual(
- param.irIsolateBitsAtSlot(meta.maskSlot, includeStableBit = true),
+ param.irIsolateBitsAtSlot(meta.maskSlot, includeStableBit = false),
irConst(ParamState.Different.bitsForSlot(meta.maskSlot))
)
)
@@ -3702,6 +3735,14 @@
)
}
+ override fun irSlotAnd(slot: Int, bits: Int): IrExpression {
+ // %changed and 0b11
+ return irAnd(
+ irGet(params[paramIndexForSlot(slot)]),
+ irBitsForSlot(bits, slot)
+ )
+ }
+
override fun irHasDifferences(): IrExpression {
if (count == 0) {
// for 0 slots (no params), we can create a shortcut expression of just checking the
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 d45ddd1..4bdb3e6 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
@@ -88,6 +88,10 @@
return super.visitFunction(declaration).also { it.copyMetadataFrom(declaration) }
}
+ override fun visitConstructor(declaration: IrConstructor): IrConstructor {
+ return super.visitConstructor(declaration).also { it.copyMetadataFrom(declaration) }
+ }
+
override fun visitSimpleFunction(declaration: IrSimpleFunction): IrSimpleFunction {
return super.visitSimpleFunction(declaration).also {
it.correspondingPropertySymbol = declaration.correspondingPropertySymbol
@@ -284,6 +288,7 @@
is IrPropertyImpl -> metadata = owner.metadata
is IrFunction -> metadata = owner.metadata
is IrClassImpl -> metadata = owner.metadata
+ else -> throw Error("Unknown type: $this")
}
}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
index d9cdd91..e4c97a5 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
@@ -356,7 +356,16 @@
).also { fn ->
newDescriptor.bind(fn)
if (this is IrSimpleFunction) {
- fn.correspondingPropertySymbol = correspondingPropertySymbol
+ val propertySymbol = correspondingPropertySymbol
+ if (propertySymbol != null) {
+ fn.correspondingPropertySymbol = propertySymbol
+ if (propertySymbol.owner.getter == this) {
+ propertySymbol.owner.getter = fn
+ }
+ if (propertySymbol.owner.setter == this) {
+ propertySymbol.owner.setter = this
+ }
+ }
}
fn.parent = parent
fn.typeParameters = this.typeParameters.map {
diff --git a/compose/desktop/desktop/build.gradle b/compose/desktop/desktop/build.gradle
index 5a631af..328931fc 100644
--- a/compose/desktop/desktop/build.gradle
+++ b/compose/desktop/desktop/build.gradle
@@ -21,6 +21,7 @@
import androidx.build.SupportConfigKt
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import static androidx.build.AndroidXPlugin.BUILD_ON_SERVER_TASK
import static androidx.build.dependencies.DependenciesKt.*
plugins {
@@ -124,6 +125,7 @@
}
}
-rootProject.tasks.getByName("buildOnServer").configure {
- dependsOn(":compose:desktop:desktop:jar")
+def projectPath = project.path
+rootProject.tasks.named(BUILD_ON_SERVER_TASK).configure {
+ dependsOn("$projectPath:jvmJar")
}
\ No newline at end of file
diff --git a/compose/desktop/desktop/samples/build.gradle b/compose/desktop/desktop/samples/build.gradle
index 12c04c9..f81a739 100644
--- a/compose/desktop/desktop/samples/build.gradle
+++ b/compose/desktop/desktop/samples/build.gradle
@@ -18,6 +18,7 @@
import androidx.build.SupportConfigKt
+import static androidx.build.AndroidXPlugin.BUILD_ON_SERVER_TASK
import static androidx.build.dependencies.DependenciesKt.*
plugins {
@@ -91,3 +92,8 @@
task run {
dependsOn("run1")
}
+
+def projectPath = project.path
+rootProject.tasks.named(BUILD_ON_SERVER_TASK).configure {
+ dependsOn("$projectPath:jvmJar")
+}
\ No newline at end of file
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.kt
index 23699a4..fb54ac1 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.kt
@@ -46,7 +46,7 @@
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material.BottomAppBar
import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants
+import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Checkbox
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.ExtendedFloatingActionButton
@@ -72,7 +72,6 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.input.key.ExperimentalKeyInput
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.plus
import androidx.compose.ui.input.key.shortcuts
@@ -133,7 +132,7 @@
IconButton(
onClick = {}
) {
- Icon(Icons.Filled.Menu, Modifier.size(ButtonConstants.DefaultIconSize))
+ Icon(Icons.Filled.Menu, Modifier.size(ButtonDefaults.IconSize))
}
}
},
@@ -158,7 +157,6 @@
)
}
-@OptIn(ExperimentalKeyInput::class)
@Composable
private fun ScrollableContent(scrollState: ScrollState) {
val amount = remember { mutableStateOf(0) }
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt
index 163625a..2dc8318 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt
@@ -36,7 +36,7 @@
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Text
import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants
+import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Checkbox
import androidx.compose.material.RadioButton
import androidx.compose.material.Surface
@@ -317,7 +317,7 @@
val buttonHover = remember { mutableStateOf(false) }
Button(
onClick = onClick,
- colors = ButtonConstants.defaultButtonColors(
+ colors = ButtonDefaults.buttonColors(
backgroundColor =
if (buttonHover.value)
Color(color.red / 1.3f, color.green / 1.3f, color.blue / 1.3f)
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt
index fb477e1..bfb6073 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt
@@ -22,31 +22,31 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.Text
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.Surface
+import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
-import javax.swing.JFrame
import javax.swing.JButton
+import javax.swing.JFrame
import javax.swing.WindowConstants
val northClicks = mutableStateOf(0)
diff --git a/compose/foundation/foundation-layout/build.gradle b/compose/foundation/foundation-layout/build.gradle
index 0d9b4e1..af73d79 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -118,7 +118,6 @@
tasks.withType(KotlinCompile).configureEach {
kotlinOptions {
- freeCompilerArgs += ["-XXLanguage:-NewInference"]
useIR = true
}
}
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutAlignTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutAlignTest.kt
index 7a9fe53..089ff61 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutAlignTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutAlignTest.kt
@@ -70,7 +70,7 @@
}
assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(IntSize(root.width, root.height), alignSize.value)
@@ -111,7 +111,7 @@
}
assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(IntSize(root.width, root.height), alignSize.value)
@@ -157,7 +157,7 @@
}
assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -231,7 +231,7 @@
}
assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(IntSize(size, size), alignSize.value)
@@ -472,7 +472,7 @@
}
positionedLatch.await(1, TimeUnit.SECONDS)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(IntSize(childSizeIpx, childSizeIpx), childSize.value)
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutPaddingTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutPaddingTest.kt
index 06c69bc..cd5004b 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutPaddingTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutPaddingTest.kt
@@ -298,7 +298,7 @@
}
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val rootWidth = root.width
@@ -361,7 +361,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val rootWidth = root.width
@@ -451,7 +451,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val innerSize = (size - paddingPx * 2)
@@ -499,7 +499,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val paddingLeft = left.toIntPx()
@@ -553,7 +553,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(IntSize(0, 0), childSize)
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutSizeTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutSizeTest.kt
index 6e71283..9130a42 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutSizeTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutSizeTest.kt
@@ -17,6 +17,7 @@
package androidx.compose.foundation.layout
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.emptyContent
import androidx.compose.ui.Alignment
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.Modifier
@@ -1283,10 +1284,16 @@
expectedConstraints: Constraints
) {
val latch = CountDownLatch(1)
+ // Capture constraints and assert on test thread
+ var actualConstraints: Constraints? = null
+ // Clear contents before each test so that we don't recompose the WithConstraints call;
+ // doing so would recompose the old subcomposition with old constraints in the presence of
+ // new content before the measurement performs explicit composition the new constraints.
+ show(emptyContent())
show {
Layout({
WithConstraints(modifier) {
- assertEquals(expectedConstraints, constraints)
+ actualConstraints = constraints
latch.countDown()
}
}) { measurables, _ ->
@@ -1295,6 +1302,7 @@
}
}
assertTrue(latch.await(1, TimeUnit.SECONDS))
+ assertEquals(expectedConstraints, actualConstraints)
}
private fun verifyIntrinsicMeasurements(expandedModifier: Modifier) = with(density) {
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutTest.kt
index 2fdccc7..722150b 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutTest.kt
@@ -36,7 +36,7 @@
import androidx.compose.ui.node.Ref
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.AmbientDensity
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.platform.ViewRootForTest
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
@@ -95,22 +95,22 @@
activityTestRule.runOnUiThread(runnable)
}
- internal fun findOwnerView(): View {
- return findOwner(activity).view
+ internal fun findComposeView(): View {
+ return findViewRootForTest(activity).view
}
- internal fun findOwner(activity: Activity): AndroidOwner {
+ internal fun findViewRootForTest(activity: Activity): ViewRootForTest {
val contentViewGroup = activity.findViewById<ViewGroup>(android.R.id.content)
- return findOwner(contentViewGroup)!!
+ return findViewRootForTest(contentViewGroup)!!
}
- internal fun findOwner(parent: ViewGroup): AndroidOwner? {
+ internal fun findViewRootForTest(parent: ViewGroup): ViewRootForTest? {
for (index in 0 until parent.childCount) {
val child = parent.getChildAt(index)
- if (child is AndroidOwner) {
+ if (child is ViewRootForTest) {
return child
} else if (child is ViewGroup) {
- val owner = findOwner(child)
+ val owner = findViewRootForTest(child)
if (owner != null) {
return owner
}
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
index bd823de..b9a80bf 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
@@ -109,7 +109,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(IntSize(size, size), childSize[0])
@@ -161,7 +161,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val rootWidth = root.width
@@ -218,7 +218,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(IntSize(childrenWidth, childrenHeight), childSize[0])
@@ -263,7 +263,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(IntSize(size, size), childSize[0])
@@ -315,7 +315,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val rootHeight = root.height
@@ -369,7 +369,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(IntSize(childrenWidth, childrenHeight), childSize[0])
@@ -624,7 +624,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(IntSize(size, root.height), childSize[0])
@@ -682,7 +682,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val rootHeight = root.height
@@ -755,7 +755,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val rootHeight = root.height
@@ -976,7 +976,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(IntSize(root.width, size), childSize[0])
@@ -1035,7 +1035,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val rootWidth = root.width
@@ -1104,7 +1104,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val rootWidth = root.width
@@ -1286,7 +1286,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -1316,7 +1316,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -1355,7 +1355,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -1385,7 +1385,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -1415,7 +1415,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -1448,7 +1448,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -1490,7 +1490,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -1523,7 +1523,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -1556,7 +1556,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -1589,7 +1589,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -1638,7 +1638,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -1756,7 +1756,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -1786,7 +1786,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -1825,7 +1825,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -1855,7 +1855,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -1885,7 +1885,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -1918,7 +1918,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -1960,7 +1960,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -1993,7 +1993,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -2026,7 +2026,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -2059,7 +2059,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -2109,7 +2109,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -2244,7 +2244,7 @@
calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(Offset(0f, 0f), childPosition[0])
@@ -2290,7 +2290,7 @@
calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(Offset((root.width - size.toFloat() * 3), 0f), childPosition[0])
@@ -2336,7 +2336,7 @@
calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val extraSpace = root.width - size * 3
@@ -2392,7 +2392,7 @@
calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val gap = (root.width - size.toFloat() * 3f) / 4f
@@ -2447,7 +2447,7 @@
calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val gap = (root.width - size.toFloat() * 3) / 2
@@ -2500,7 +2500,7 @@
calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val gap = (root.width.toFloat() - size * 3) / 3
@@ -2705,7 +2705,7 @@
calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(Offset(0f, 0f), childPosition[0])
@@ -2751,7 +2751,7 @@
calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(Offset(0f, (root.height - size.toFloat() * 3)), childPosition[0])
@@ -2797,7 +2797,7 @@
calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val extraSpace = root.height - size * 3f
@@ -2856,7 +2856,7 @@
calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val gap = (root.height - size.toFloat() * 3) / 4
@@ -2920,7 +2920,7 @@
calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val gap = (root.height - size.toFloat() * 3f) / 2f
@@ -2973,7 +2973,7 @@
calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val gap = (root.height - size.toFloat() * 3f) / 3f
@@ -4047,7 +4047,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val rootWidth = root.width
@@ -4096,7 +4096,7 @@
calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val extraSpace = root.width - size * 3
@@ -4152,7 +4152,7 @@
calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val gap = (root.width - size.toFloat() * 3f) / 4f
@@ -4207,7 +4207,7 @@
calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val gap = (root.width - size.toFloat() * 3) / 2
@@ -4260,7 +4260,7 @@
calculateChildPositions(childPosition, parentLayoutCoordinates, childLayoutCoordinates)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val gap = (root.width.toFloat() - size * 3) / 3
@@ -4388,7 +4388,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val rootWidth = root.width
@@ -4475,7 +4475,7 @@
}
assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val rootWidth = root.width
@@ -4535,7 +4535,7 @@
childLayoutCoordinates
)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(Offset(0f, 0f), childPosition[0])
@@ -4590,7 +4590,7 @@
childLayoutCoordinates
)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(Offset(0f, 0f), childPosition[0])
@@ -4642,7 +4642,7 @@
childLayoutCoordinates
)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -4703,7 +4703,7 @@
childLayoutCoordinates
)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
assertEquals(
@@ -4761,7 +4761,7 @@
childLayoutCoordinates
)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val extraSpace = root.width - size * 3
@@ -4832,7 +4832,7 @@
childLayoutCoordinates
)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val extraSpace = root.width - size * 3
@@ -4900,7 +4900,7 @@
childLayoutCoordinates
)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val gap = (root.width - size.toFloat() * 3f) / 4f
@@ -4969,7 +4969,7 @@
childLayoutCoordinates
)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val gap = (root.width - size.toFloat() * 3f) / 4f
@@ -5034,7 +5034,7 @@
childLayoutCoordinates
)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val gap = (root.width - size.toFloat() * 3) / 2
@@ -5101,7 +5101,7 @@
childLayoutCoordinates
)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val gap = (root.width - size.toFloat() * 3) / 2
@@ -5163,7 +5163,7 @@
childLayoutCoordinates
)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val gap = (root.width.toFloat() - size * 3) / 3
@@ -5233,7 +5233,7 @@
childLayoutCoordinates
)
- val root = findOwnerView()
+ val root = findComposeView()
waitForDraw(root)
val gap = (root.width.toFloat() - size * 3) / 3
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Arrangement.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Arrangement.kt
index 9589035..36f9646 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Arrangement.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Arrangement.kt
@@ -27,14 +27,14 @@
import kotlin.math.roundToInt
/**
- * Used to specify the arrangement of the layout's children in [Row] or [Column] in the main axis
- * direction (horizontal and vertical, respectively).
+ * Used to specify the arrangement of the layout's children in layouts like [Row] or [Column] in
+ * the main axis direction (horizontal and vertical, respectively).
*/
@Immutable
@OptIn(InternalLayoutApi::class)
object Arrangement {
/**
- * Used to specify the horizontal arrangement of the layout's children in a [Row].
+ * Used to specify the horizontal arrangement of the layout's children in layouts like [Row].
*/
@InternalLayoutApi
@Immutable
@@ -45,7 +45,7 @@
val spacing get() = 0.dp
/**
- * Horizontally places the layout children inside the [Row].
+ * Horizontally places the layout children.
*
* @param totalSize Available space that can be occupied by the children.
* @param size An array of sizes of all children.
@@ -64,7 +64,7 @@
}
/**
- * Used to specify the vertical arrangement of the layout's children in a [Column].
+ * Used to specify the vertical arrangement of the layout's children in layouts like [Column].
*/
@InternalLayoutApi
@Immutable
@@ -75,7 +75,7 @@
val spacing get() = 0.dp
/**
- * Vertically places the layout children inside the [Column].
+ * Vertically places the layout children.
*
* @param totalSize Available space that can be occupied by the children.
* @param size An array of sizes of all children.
@@ -91,8 +91,9 @@
}
/**
- * Used to specify the horizontal arrangement of the layout's children in a [Row], or
- * the vertical arrangement of the layout's children in a [Column].
+ * Used to specify the horizontal arrangement of the layout's children in horizontal layouts
+ * like [Row], or the vertical arrangement of the layout's children in vertical layouts like
+ * [Column].
*/
@InternalLayoutApi
@Immutable
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index ddb6503..0d949bc 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -232,18 +232,18 @@
}
public final class DragGestureDetectorKt {
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitHorizontalDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitHorizontalTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitVerticalDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitVerticalTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectHorizontalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onHorizontalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectVerticalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onVerticalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? drag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? horizontalDrag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? verticalDrag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method public static suspend Object? awaitDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? awaitHorizontalDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? awaitHorizontalTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? awaitTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? awaitVerticalDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? awaitVerticalTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? detectDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend Object? detectHorizontalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onHorizontalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend Object? detectVerticalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onVerticalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend Object? drag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method public static suspend Object? horizontalDrag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method public static suspend Object? verticalDrag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
}
public final class DraggableKt {
@@ -251,7 +251,7 @@
}
public final class ForEachGestureKt {
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? forEachGesture(androidx.compose.ui.input.pointer.PointerInputScope, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend Object? forEachGesture(androidx.compose.ui.input.pointer.PointerInputScope, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
}
public final class GestureCancellationException extends java.util.concurrent.CancellationException {
@@ -265,7 +265,7 @@
method public static long calculatePan(androidx.compose.ui.input.pointer.PointerEvent);
method public static float calculateRotation(androidx.compose.ui.input.pointer.PointerEvent);
method public static float calculateZoom(androidx.compose.ui.input.pointer.PointerEvent);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectMultitouchGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional boolean panZoomLock, kotlin.jvm.functions.Function4<? super androidx.compose.ui.geometry.Offset,? super androidx.compose.ui.geometry.Offset,? super java.lang.Float,? super java.lang.Float,kotlin.Unit> onGesture, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend Object? detectMultitouchGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional boolean panZoomLock, kotlin.jvm.functions.Function4<? super androidx.compose.ui.geometry.Offset,? super androidx.compose.ui.geometry.Offset,? super java.lang.Float,? super java.lang.Float,kotlin.Unit> onGesture, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
}
public interface PressGestureScope extends androidx.compose.ui.unit.Density {
@@ -293,9 +293,9 @@
}
public final class TapGestureDetectorKt {
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.HandlePointerInputScope, optional boolean requireUnconsumed, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectTapGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onDoubleTap, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongPress, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.PressGestureScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onPress, kotlin.jvm.functions.Function0<kotlin.Unit> onTap, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.HandlePointerInputScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.HandlePointerInputScope, optional boolean requireUnconsumed, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? detectTapGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onDoubleTap, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongPress, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.PressGestureScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onPress, kotlin.jvm.functions.Function0<kotlin.Unit> onTap, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.HandlePointerInputScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
}
public final class ZoomableController {
@@ -320,15 +320,15 @@
}
public final class LazyDslKt {
- method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
}
public final class LazyForKt {
- method @androidx.compose.runtime.Composable public static <T> void LazyColumnFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
- method @androidx.compose.runtime.Composable public static <T> void LazyColumnForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
- method @androidx.compose.runtime.Composable public static <T> void LazyRowFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
- method @androidx.compose.runtime.Composable public static <T> void LazyRowForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyColumnFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+ method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyColumnForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyRowFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+ method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyRowForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
}
public final class LazyGridKt {
@@ -340,9 +340,29 @@
method public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
}
+ public interface LazyListItemInfo {
+ method public int getIndex();
+ method public int getOffset();
+ method public int getSize();
+ property public abstract int index;
+ property public abstract int offset;
+ property public abstract int size;
+ }
+
public final class LazyListKt {
}
+ public interface LazyListLayoutInfo {
+ method public int getTotalItemsCount();
+ method public int getViewportEndOffset();
+ method public int getViewportStartOffset();
+ method public java.util.List<androidx.compose.foundation.lazy.LazyListItemInfo> getVisibleItemsInfo();
+ property public abstract int totalItemsCount;
+ property public abstract int viewportEndOffset;
+ property public abstract int viewportStartOffset;
+ property public abstract java.util.List<androidx.compose.foundation.lazy.LazyListItemInfo> visibleItemsInfo;
+ }
+
public final class LazyListMeasureKt {
}
@@ -356,6 +376,7 @@
ctor public LazyListState(int firstVisibleItemIndex, int firstVisibleItemScrollOffset, androidx.compose.foundation.InteractionState? interactionState, androidx.compose.foundation.animation.FlingConfig flingConfig, androidx.compose.animation.core.AnimationClockObservable animationClock);
method public int getFirstVisibleItemIndex();
method public int getFirstVisibleItemScrollOffset();
+ method public androidx.compose.foundation.lazy.LazyListLayoutInfo getLayoutInfo();
method public boolean isAnimationRunning();
method public suspend Object? scroll(kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public suspend Object? smoothScrollBy(float value, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> spec, optional kotlin.coroutines.Continuation<? super java.lang.Float> p);
@@ -363,6 +384,7 @@
property public final int firstVisibleItemIndex;
property public final int firstVisibleItemScrollOffset;
property public final boolean isAnimationRunning;
+ property public final androidx.compose.foundation.lazy.LazyListLayoutInfo layoutInfo;
field public static final androidx.compose.foundation.lazy.LazyListState.Companion Companion;
}
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index ddb6503..0d949bc 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -232,18 +232,18 @@
}
public final class DragGestureDetectorKt {
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitHorizontalDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitHorizontalTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitVerticalDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitVerticalTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectHorizontalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onHorizontalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectVerticalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onVerticalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? drag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? horizontalDrag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? verticalDrag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method public static suspend Object? awaitDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? awaitHorizontalDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? awaitHorizontalTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? awaitTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? awaitVerticalDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? awaitVerticalTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? detectDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend Object? detectHorizontalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onHorizontalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend Object? detectVerticalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onVerticalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend Object? drag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method public static suspend Object? horizontalDrag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method public static suspend Object? verticalDrag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
}
public final class DraggableKt {
@@ -251,7 +251,7 @@
}
public final class ForEachGestureKt {
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? forEachGesture(androidx.compose.ui.input.pointer.PointerInputScope, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend Object? forEachGesture(androidx.compose.ui.input.pointer.PointerInputScope, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
}
public final class GestureCancellationException extends java.util.concurrent.CancellationException {
@@ -265,7 +265,7 @@
method public static long calculatePan(androidx.compose.ui.input.pointer.PointerEvent);
method public static float calculateRotation(androidx.compose.ui.input.pointer.PointerEvent);
method public static float calculateZoom(androidx.compose.ui.input.pointer.PointerEvent);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectMultitouchGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional boolean panZoomLock, kotlin.jvm.functions.Function4<? super androidx.compose.ui.geometry.Offset,? super androidx.compose.ui.geometry.Offset,? super java.lang.Float,? super java.lang.Float,kotlin.Unit> onGesture, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend Object? detectMultitouchGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional boolean panZoomLock, kotlin.jvm.functions.Function4<? super androidx.compose.ui.geometry.Offset,? super androidx.compose.ui.geometry.Offset,? super java.lang.Float,? super java.lang.Float,kotlin.Unit> onGesture, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
}
public interface PressGestureScope extends androidx.compose.ui.unit.Density {
@@ -293,9 +293,9 @@
}
public final class TapGestureDetectorKt {
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.HandlePointerInputScope, optional boolean requireUnconsumed, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectTapGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onDoubleTap, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongPress, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.PressGestureScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onPress, kotlin.jvm.functions.Function0<kotlin.Unit> onTap, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.HandlePointerInputScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.HandlePointerInputScope, optional boolean requireUnconsumed, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? detectTapGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onDoubleTap, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongPress, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.PressGestureScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onPress, kotlin.jvm.functions.Function0<kotlin.Unit> onTap, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.HandlePointerInputScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
}
public final class ZoomableController {
@@ -320,15 +320,15 @@
}
public final class LazyDslKt {
- method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
}
public final class LazyForKt {
- method @androidx.compose.runtime.Composable public static <T> void LazyColumnFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
- method @androidx.compose.runtime.Composable public static <T> void LazyColumnForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
- method @androidx.compose.runtime.Composable public static <T> void LazyRowFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
- method @androidx.compose.runtime.Composable public static <T> void LazyRowForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyColumnFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+ method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyColumnForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyRowFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+ method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyRowForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
}
public final class LazyGridKt {
@@ -340,9 +340,29 @@
method public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
}
+ public interface LazyListItemInfo {
+ method public int getIndex();
+ method public int getOffset();
+ method public int getSize();
+ property public abstract int index;
+ property public abstract int offset;
+ property public abstract int size;
+ }
+
public final class LazyListKt {
}
+ public interface LazyListLayoutInfo {
+ method public int getTotalItemsCount();
+ method public int getViewportEndOffset();
+ method public int getViewportStartOffset();
+ method public java.util.List<androidx.compose.foundation.lazy.LazyListItemInfo> getVisibleItemsInfo();
+ property public abstract int totalItemsCount;
+ property public abstract int viewportEndOffset;
+ property public abstract int viewportStartOffset;
+ property public abstract java.util.List<androidx.compose.foundation.lazy.LazyListItemInfo> visibleItemsInfo;
+ }
+
public final class LazyListMeasureKt {
}
@@ -356,6 +376,7 @@
ctor public LazyListState(int firstVisibleItemIndex, int firstVisibleItemScrollOffset, androidx.compose.foundation.InteractionState? interactionState, androidx.compose.foundation.animation.FlingConfig flingConfig, androidx.compose.animation.core.AnimationClockObservable animationClock);
method public int getFirstVisibleItemIndex();
method public int getFirstVisibleItemScrollOffset();
+ method public androidx.compose.foundation.lazy.LazyListLayoutInfo getLayoutInfo();
method public boolean isAnimationRunning();
method public suspend Object? scroll(kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public suspend Object? smoothScrollBy(float value, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> spec, optional kotlin.coroutines.Continuation<? super java.lang.Float> p);
@@ -363,6 +384,7 @@
property public final int firstVisibleItemIndex;
property public final int firstVisibleItemScrollOffset;
property public final boolean isAnimationRunning;
+ property public final androidx.compose.foundation.lazy.LazyListLayoutInfo layoutInfo;
field public static final androidx.compose.foundation.lazy.LazyListState.Companion Companion;
}
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index ddb6503..0d949bc 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -232,18 +232,18 @@
}
public final class DragGestureDetectorKt {
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitHorizontalDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitHorizontalTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitVerticalDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitVerticalTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectHorizontalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onHorizontalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectVerticalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onVerticalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? drag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? horizontalDrag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? verticalDrag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method public static suspend Object? awaitDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? awaitHorizontalDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? awaitHorizontalTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? awaitTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? awaitVerticalDragOrCancellation-3UZYup8(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? awaitVerticalTouchSlopOrCancellation-s7qLkbw(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onTouchSlopReached, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? detectDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super androidx.compose.ui.geometry.Offset,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend Object? detectHorizontalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onHorizontalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend Object? detectVerticalDragGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragEnd, optional kotlin.jvm.functions.Function0<kotlin.Unit> onDragCancel, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputChange,? super java.lang.Float,kotlin.Unit> onVerticalDrag, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend Object? drag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method public static suspend Object? horizontalDrag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method public static suspend Object? verticalDrag-xpXNQDM(androidx.compose.ui.input.pointer.HandlePointerInputScope, long pointerId, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.pointer.PointerInputChange,kotlin.Unit> onDrag, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
}
public final class DraggableKt {
@@ -251,7 +251,7 @@
}
public final class ForEachGestureKt {
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? forEachGesture(androidx.compose.ui.input.pointer.PointerInputScope, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend Object? forEachGesture(androidx.compose.ui.input.pointer.PointerInputScope, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
}
public final class GestureCancellationException extends java.util.concurrent.CancellationException {
@@ -265,7 +265,7 @@
method public static long calculatePan(androidx.compose.ui.input.pointer.PointerEvent);
method public static float calculateRotation(androidx.compose.ui.input.pointer.PointerEvent);
method public static float calculateZoom(androidx.compose.ui.input.pointer.PointerEvent);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectMultitouchGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional boolean panZoomLock, kotlin.jvm.functions.Function4<? super androidx.compose.ui.geometry.Offset,? super androidx.compose.ui.geometry.Offset,? super java.lang.Float,? super java.lang.Float,kotlin.Unit> onGesture, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend Object? detectMultitouchGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional boolean panZoomLock, kotlin.jvm.functions.Function4<? super androidx.compose.ui.geometry.Offset,? super androidx.compose.ui.geometry.Offset,? super java.lang.Float,? super java.lang.Float,kotlin.Unit> onGesture, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
}
public interface PressGestureScope extends androidx.compose.ui.unit.Density {
@@ -293,9 +293,9 @@
}
public final class TapGestureDetectorKt {
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.HandlePointerInputScope, optional boolean requireUnconsumed, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? detectTapGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onDoubleTap, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongPress, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.PressGestureScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onPress, kotlin.jvm.functions.Function0<kotlin.Unit> onTap, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.HandlePointerInputScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.HandlePointerInputScope, optional boolean requireUnconsumed, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
+ method public static suspend Object? detectTapGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onDoubleTap, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongPress, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.PressGestureScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onPress, kotlin.jvm.functions.Function0<kotlin.Unit> onTap, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.HandlePointerInputScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange> p);
}
public final class ZoomableController {
@@ -320,15 +320,15 @@
}
public final class LazyDslKt {
- method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void LazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void LazyRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyListScope,kotlin.Unit> content);
}
public final class LazyForKt {
- method @androidx.compose.runtime.Composable public static <T> void LazyColumnFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
- method @androidx.compose.runtime.Composable public static <T> void LazyColumnForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
- method @androidx.compose.runtime.Composable public static <T> void LazyRowFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
- method @androidx.compose.runtime.Composable public static <T> void LazyRowForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyColumnFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+ method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyColumnForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
+ method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyRowFor(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super T,kotlin.Unit> itemContent);
+ method @Deprecated @androidx.compose.runtime.Composable public static <T> void LazyRowForIndexed(java.util.List<? extends T> items, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.LazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.ui.Alignment.Vertical verticalAlignment, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
}
public final class LazyGridKt {
@@ -340,9 +340,29 @@
method public androidx.compose.ui.Modifier fillParentMaxWidth(androidx.compose.ui.Modifier, optional @FloatRange(from=0.0, to=1.0) float fraction);
}
+ public interface LazyListItemInfo {
+ method public int getIndex();
+ method public int getOffset();
+ method public int getSize();
+ property public abstract int index;
+ property public abstract int offset;
+ property public abstract int size;
+ }
+
public final class LazyListKt {
}
+ public interface LazyListLayoutInfo {
+ method public int getTotalItemsCount();
+ method public int getViewportEndOffset();
+ method public int getViewportStartOffset();
+ method public java.util.List<androidx.compose.foundation.lazy.LazyListItemInfo> getVisibleItemsInfo();
+ property public abstract int totalItemsCount;
+ property public abstract int viewportEndOffset;
+ property public abstract int viewportStartOffset;
+ property public abstract java.util.List<androidx.compose.foundation.lazy.LazyListItemInfo> visibleItemsInfo;
+ }
+
public final class LazyListMeasureKt {
}
@@ -356,6 +376,7 @@
ctor public LazyListState(int firstVisibleItemIndex, int firstVisibleItemScrollOffset, androidx.compose.foundation.InteractionState? interactionState, androidx.compose.foundation.animation.FlingConfig flingConfig, androidx.compose.animation.core.AnimationClockObservable animationClock);
method public int getFirstVisibleItemIndex();
method public int getFirstVisibleItemScrollOffset();
+ method public androidx.compose.foundation.lazy.LazyListLayoutInfo getLayoutInfo();
method public boolean isAnimationRunning();
method public suspend Object? scroll(kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public suspend Object? smoothScrollBy(float value, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> spec, optional kotlin.coroutines.Continuation<? super java.lang.Float> p);
@@ -363,6 +384,7 @@
property public final int firstVisibleItemIndex;
property public final int firstVisibleItemScrollOffset;
property public final boolean isAnimationRunning;
+ property public final androidx.compose.foundation.lazy.LazyListLayoutInfo layoutInfo;
field public static final androidx.compose.foundation.lazy.LazyListState.Companion Companion;
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
index 33ca738..b44a23a 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
@@ -35,6 +35,7 @@
ComposableDemo("Multiple-interaction InteractionState") {
MultipleInteractionStateSample()
},
- DemoCategory("Suspending Gesture Detectors", CoroutineGestureDemos)
+ DemoCategory("Suspending Gesture Detectors", CoroutineGestureDemos),
+ ComposableDemo("NestedScroll") { NestedScrollDemo() },
)
)
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
index f804404..d3d049e 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
@@ -18,8 +18,11 @@
import androidx.compose.foundation.Interaction
import androidx.compose.foundation.InteractionState
+import androidx.compose.foundation.ScrollableColumn
import androidx.compose.foundation.background
+import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayout
@@ -28,16 +31,14 @@
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.preferredWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyColumnFor
-import androidx.compose.foundation.lazy.LazyColumnForIndexed
import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.LazyRowFor
-import androidx.compose.foundation.lazy.LazyRowForIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.integration.demos.common.ComposableDemo
import androidx.compose.material.AmbientContentColor
@@ -49,6 +50,7 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.savedinstancestate.savedInstanceState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -73,22 +75,27 @@
ComposableDemo("Rtl list") { RtlListDemo() },
ComposableDemo("LazyColumn DSL") { LazyColumnScope() },
ComposableDemo("LazyRow DSL") { LazyRowScope() },
+ ComposableDemo("Arrangements") { LazyListArrangements() },
+ ComposableDemo("Reverse scroll direction") { ReverseLayout() },
+ ComposableDemo("Nested lazy lists") { NestedLazyDemo() },
PagingDemos
)
@Composable
private fun LazyColumnDemo() {
- LazyColumnFor(
- items = listOf(
- "Hello,", "World:", "It works!", "",
- "this one is really long and spans a few lines for scrolling purposes",
- "these", "are", "offscreen"
- ) + (1..100).map { "$it" }
- ) {
- Text(text = it, fontSize = 80.sp)
+ LazyColumn {
+ items(
+ items = listOf(
+ "Hello,", "World:", "It works!", "",
+ "this one is really long and spans a few lines for scrolling purposes",
+ "these", "are", "offscreen"
+ ) + (1..100).map { "$it" }
+ ) {
+ Text(text = it, fontSize = 80.sp)
- if (it.contains("works")) {
- Text("You can even emit multiple components per item.")
+ if (it.contains("works")) {
+ Text("You can even emit multiple components per item.")
+ }
}
}
}
@@ -104,11 +111,10 @@
Button(modifier = buttonModifier, onClick = { numItems-- }) { Text("Remove") }
Button(modifier = buttonModifier, onClick = { offset++ }) { Text("Offset") }
}
- LazyColumnFor(
- (1..numItems).map { it + offset }.toList(),
- Modifier.fillMaxWidth()
- ) {
- Text("$it", style = AmbientTextStyle.current.copy(fontSize = 40.sp))
+ LazyColumn(Modifier.fillMaxWidth()) {
+ items((1..numItems).map { it + offset }.toList()) {
+ Text("$it", style = AmbientTextStyle.current.copy(fontSize = 40.sp))
+ }
}
}
}
@@ -174,18 +180,19 @@
fontSize = 20.sp
)
}
- LazyColumnFor(
- (0..1000).toList(),
+ LazyColumn(
Modifier.fillMaxWidth(),
state = state
) {
- Text("$it", style = AmbientTextStyle.current.copy(fontSize = 40.sp))
+ items((0..1000).toList()) {
+ Text("$it", style = AmbientTextStyle.current.copy(fontSize = 40.sp))
+ }
}
}
}
@Composable
-fun Button(modifier: Modifier, onClick: () -> Unit, content: @Composable () -> Unit) {
+fun Button(modifier: Modifier = Modifier, onClick: () -> Unit, content: @Composable () -> Unit) {
Box(
modifier
.clickable(onClick = onClick)
@@ -198,8 +205,10 @@
@Composable
private fun LazyRowItemsDemo() {
- LazyRowFor(items = (1..1000).toList()) {
- Square(it)
+ LazyRow {
+ items((1..1000).toList()) {
+ Square(it)
+ }
}
}
@@ -218,11 +227,15 @@
private fun ListWithIndexSample() {
val friends = listOf("Alex", "John", "Danny", "Sam")
Column {
- LazyRowForIndexed(friends, Modifier.fillMaxWidth()) { index, friend ->
- Text("$friend at index $index", Modifier.padding(16.dp))
+ LazyRow(Modifier.fillMaxWidth()) {
+ itemsIndexed(friends) { index, friend ->
+ Text("$friend at index $index", Modifier.padding(16.dp))
+ }
}
- LazyColumnForIndexed(friends, Modifier.fillMaxWidth()) { index, friend ->
- Text("$friend at index $index", Modifier.padding(16.dp))
+ LazyColumn(Modifier.fillMaxWidth()) {
+ itemsIndexed(friends) { index, friend ->
+ Text("$friend at index $index", Modifier.padding(16.dp))
+ }
}
}
}
@@ -230,14 +243,16 @@
@Composable
private fun RtlListDemo() {
Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
- LazyRowForIndexed((0..100).toList(), Modifier.fillMaxWidth()) { index, item ->
- Text(
- "$item",
- Modifier
- .size(100.dp)
- .background(if (index % 2 == 0) Color.LightGray else Color.Transparent)
- .padding(16.dp)
- )
+ LazyRow(Modifier.fillMaxWidth()) {
+ itemsIndexed((0..100).toList()) { index, item ->
+ Text(
+ "$item",
+ Modifier
+ .size(100.dp)
+ .background(if (index % 2 == 0) Color.LightGray else Color.Transparent)
+ .padding(16.dp)
+ )
+ }
}
}
}
@@ -245,8 +260,10 @@
@Composable
private fun PagerLikeDemo() {
val pages = listOf(Color.LightGray, Color.White, Color.DarkGray)
- LazyRowFor(pages) {
- Spacer(Modifier.fillParentMaxSize().background(it))
+ LazyRow {
+ items(pages) {
+ Spacer(Modifier.fillParentMaxSize().background(it))
+ }
}
}
@@ -297,4 +314,165 @@
}
}
}
-}
\ No newline at end of file
+}
+
+@Composable
+private fun LazyListArrangements() {
+ var count by remember { mutableStateOf(3) }
+ var arrangement by remember { mutableStateOf(6) }
+ Column {
+ Row {
+ Button(onClick = { count-- }) {
+ Text("--")
+ }
+ Button(onClick = { count++ }) {
+ Text("++")
+ }
+ Button(
+ onClick = {
+ arrangement++
+ if (arrangement == Arrangements.size) {
+ arrangement = 0
+ }
+ }
+ ) {
+ Text("Next")
+ }
+ Text("$arrangement ${Arrangements[arrangement]}")
+ }
+ Row {
+ val item = @Composable {
+ Box(
+ Modifier
+ .height(200.dp)
+ .fillMaxWidth()
+ .background(Color.Red)
+ .border(1.dp, Color.Cyan)
+ )
+ }
+ ScrollableColumn(
+ verticalArrangement = Arrangements[arrangement],
+ modifier = Modifier.weight(1f).fillMaxHeight()
+ ) {
+ (1..count).forEach {
+ item()
+ }
+ }
+ LazyColumn(
+ verticalArrangement = Arrangements[arrangement],
+ modifier = Modifier.weight(1f).fillMaxHeight()
+ ) {
+ items((1..count).toList()) {
+ item()
+ }
+ }
+ }
+ }
+}
+
+private val Arrangements = listOf(
+ Arrangement.Center,
+ Arrangement.Top,
+ Arrangement.Bottom,
+ Arrangement.SpaceAround,
+ Arrangement.SpaceBetween,
+ Arrangement.SpaceEvenly,
+ Arrangement.spacedBy(40.dp),
+ Arrangement.spacedBy(40.dp, Alignment.Bottom),
+)
+
+@Composable
+fun ReverseLayout() {
+ Column {
+ val scrollState = rememberScrollState()
+ val lazyState = rememberLazyListState()
+ var count by remember { mutableStateOf(3) }
+ var reverse by remember { mutableStateOf(true) }
+ Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ Button(onClick = { count -= 5 }) {
+ Text("--")
+ }
+ Button(onClick = { count += 5 }) {
+ Text("++")
+ }
+ Button(onClick = { reverse = !reverse }) {
+ Text("=!")
+ }
+ Text("Scroll=${scrollState.value.toInt()}")
+ Text(
+ "Lazy=${lazyState.firstVisibleItemIndex}; " +
+ "${lazyState.firstVisibleItemScrollOffset}"
+ )
+ }
+ Row {
+ val item1 = @Composable { index: Int ->
+ Text(
+ "$index",
+ Modifier
+ .height(200.dp)
+ .fillMaxWidth()
+ .background(Color.Red)
+ .border(1.dp, Color.Cyan)
+ )
+ }
+ val item2 = @Composable { index: Int ->
+ Text("After $index")
+ }
+ ScrollableColumn(
+ reverseScrollDirection = reverse,
+ verticalArrangement = if (reverse) Arrangement.Bottom else Arrangement.Top,
+ scrollState = scrollState,
+ modifier = Modifier.weight(1f).fillMaxHeight()
+ ) {
+ if (reverse) {
+ (count downTo 1).forEach {
+ item2(it)
+ item1(it)
+ }
+ } else {
+ (1..count).forEach {
+ item1(it)
+ item2(it)
+ }
+ }
+ }
+ LazyColumn(
+ reverseLayout = reverse,
+ state = lazyState,
+ modifier = Modifier.weight(1f).fillMaxHeight()
+ ) {
+ items((1..count).toList()) {
+ item1(it)
+ item2(it)
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun NestedLazyDemo() {
+ val item = @Composable { index: Int ->
+ Box(
+ Modifier.padding(16.dp).size(200.dp).background(Color.LightGray),
+ contentAlignment = Alignment.Center
+ ) {
+ var state by savedInstanceState { 0 }
+ Button(onClick = { state++ }) {
+ Text("Index=$index State=$state")
+ }
+ }
+ }
+ LazyColumn {
+ item {
+ LazyRow {
+ items(List(100) { it }) {
+ item(it)
+ }
+ }
+ }
+ items(List(100) { it }) {
+ item(it)
+ }
+ }
+}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/NestedScrollDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/NestedScrollDemos.kt
new file mode 100644
index 0000000..b4fdf7b
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/NestedScrollDemos.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.demos
+
+import androidx.compose.foundation.ScrollableColumn
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.Text
+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.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun NestedScrollDemo() {
+ ScrollableColumn(
+ Modifier.fillMaxSize().background(Color.Red),
+ contentPadding = PaddingValues(30.dp)
+ ) {
+ repeat(6) { outerOuterIndex ->
+ LazyColumn(
+ modifier = Modifier.fillMaxSize().border(3.dp, Color.Black).height(350.dp)
+ .background(Color.Yellow),
+ contentPadding = PaddingValues(60.dp)
+ ) {
+ repeat(3) { outerIndex ->
+ item {
+ ScrollableColumn(
+ Modifier.fillMaxSize().border(3.dp, Color.Blue).height(150.dp)
+ .background(Color.White),
+ contentPadding = PaddingValues(30.dp)
+ ) {
+ repeat(6) { innerIndex ->
+ Box(
+ Modifier
+ .height(38.dp)
+ .fillMaxWidth()
+ .background(Color.Magenta)
+ .border(2.dp, Color.Yellow),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = "$outerOuterIndex : $outerIndex : $innerIndex",
+ fontSize = 24.sp
+ )
+ }
+ }
+ }
+ }
+
+ item {
+ Spacer(Modifier.height(5.dp))
+ }
+ }
+ }
+ Spacer(Modifier.height(5.dp))
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/SuspendingGesturesDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/SuspendingGesturesDemo.kt
index 1310c48..a16da6a 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/SuspendingGesturesDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/SuspendingGesturesDemo.kt
@@ -51,7 +51,6 @@
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.gesture.ExperimentalPointerInput
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
@@ -107,7 +106,6 @@
/**
* Gesture detector for tap, double-tap, and long-press.
*/
-@OptIn(ExperimentalPointerInput::class)
@Composable
fun CoroutineTapDemo() {
var tapHue by remember { mutableStateOf(randomHue()) }
@@ -212,7 +210,6 @@
}
}
-@OptIn(ExperimentalPointerInput::class)
@Composable
fun TouchSlopDragGestures() {
Column {
@@ -290,7 +287,6 @@
}
}
-@OptIn(ExperimentalPointerInput::class)
@Composable
fun OrientationLockDragGestures() {
var size by remember { mutableStateOf(IntSize.Zero) }
@@ -335,7 +331,6 @@
}
}
-@OptIn(ExperimentalPointerInput::class)
@Composable
fun Drag2DGestures() {
var size by remember { mutableStateOf(IntSize.Zero) }
@@ -365,7 +360,6 @@
}
}
-@OptIn(ExperimentalPointerInput::class)
@Composable
fun MultitouchArea(
text: String,
@@ -440,7 +434,6 @@
* This is a multi-touch gesture detector, including pan, zoom, and rotation.
* The user can pan, zoom, and rotate once touch slop has been reached.
*/
-@OptIn(ExperimentalPointerInput::class)
@Composable
fun MultitouchGestureDetector() {
MultitouchArea(
@@ -458,7 +451,6 @@
* It is common to want to lean toward zoom over rotation, so this gesture detector will
* lock into zoom if the first unless the rotation passes touch slop first.
*/
-@OptIn(ExperimentalPointerInput::class)
@Composable
fun MultitouchLockGestureDetector() {
MultitouchArea(
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldFocusTransition.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldFocusTransition.kt
index 183dd1e..68e282c 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldFocusTransition.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldFocusTransition.kt
@@ -25,7 +25,6 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.savedinstancestate.savedInstanceState
import androidx.compose.runtime.setValue
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.isFocused
import androidx.compose.ui.focusObserver
@@ -37,7 +36,6 @@
import androidx.compose.ui.unit.sp
@Composable
-@OptIn(ExperimentalFocus::class)
fun TextFieldFocusTransition() {
val focusRequesters = List(6) { FocusRequester() }
@@ -51,7 +49,6 @@
}
}
-@OptIn(ExperimentalFocus::class)
@Composable
private fun TextFieldWithFocusRequesters(
focusRequester: FocusRequester,
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/DragGestureDetectorSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/DragGestureDetectorSamples.kt
index 4cd2ade..85f8cb8 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/DragGestureDetectorSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/DragGestureDetectorSamples.kt
@@ -48,7 +48,6 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.gesture.ExperimentalPointerInput
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.consumePositionChange
import androidx.compose.ui.input.pointer.pointerInput
@@ -57,7 +56,6 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
-@OptIn(ExperimentalPointerInput::class)
@Composable
@Sampled
fun AwaitHorizontalDragOrCancellationSample() {
@@ -102,7 +100,6 @@
}
}
-@OptIn(ExperimentalPointerInput::class)
@Composable
@Sampled
fun HorizontalDragSample() {
@@ -146,7 +143,6 @@
}
}
-@OptIn(ExperimentalPointerInput::class)
@Composable
@Sampled
fun DetectHorizontalDragGesturesSample() {
@@ -174,7 +170,6 @@
}
}
-@OptIn(ExperimentalPointerInput::class)
@Composable
@Sampled
fun AwaitVerticalDragOrCancellationSample() {
@@ -219,7 +214,6 @@
}
}
-@OptIn(ExperimentalPointerInput::class)
@Composable
@Sampled
fun VerticalDragSample() {
@@ -263,7 +257,6 @@
}
}
-@OptIn(ExperimentalPointerInput::class)
@Composable
@Sampled
fun DetectVerticalDragGesturesSample() {
@@ -291,7 +284,6 @@
}
}
-@OptIn(ExperimentalPointerInput::class)
@Composable
@Sampled
fun AwaitDragOrCancellationSample() {
@@ -348,7 +340,6 @@
}
}
-@OptIn(ExperimentalPointerInput::class)
@Composable
@Sampled
fun DragSample() {
@@ -404,7 +395,6 @@
}
}
-@OptIn(ExperimentalPointerInput::class)
@Composable
@Sampled
fun DetectDragGesturesSample() {
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/FocusableSample.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/FocusableSample.kt
index 66cd360..b5fded6 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/FocusableSample.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/FocusableSample.kt
@@ -26,13 +26,11 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focusRequester
@Sampled
@Composable
-@OptIn(ExperimentalFocus::class)
fun FocusableSample() {
// initialize focus requester to be able to request focus programmatically
val requester = FocusRequester()
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyForSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyForSamples.kt
deleted file mode 100644
index d1b87dd..0000000
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/LazyForSamples.kt
+++ /dev/null
@@ -1,61 +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.compose.foundation.samples
-
-import androidx.annotation.Sampled
-import androidx.compose.foundation.lazy.LazyColumnFor
-import androidx.compose.foundation.lazy.LazyColumnForIndexed
-import androidx.compose.foundation.lazy.LazyRowFor
-import androidx.compose.foundation.lazy.LazyRowForIndexed
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-
-@Sampled
-@Composable
-fun LazyColumnForSample() {
- val items = listOf("A", "B", "C")
- LazyColumnFor(items) {
- Text("Item is $it")
- }
-}
-
-@Sampled
-@Composable
-fun LazyRowForSample() {
- val items = listOf("A", "B", "C")
- LazyRowFor(items) {
- Text("Item is $it")
- }
-}
-
-@Sampled
-@Composable
-fun LazyColumnForIndexedSample() {
- val items = listOf("A", "B", "C")
- LazyColumnForIndexed(items) { index, item ->
- Text("Item at index $index is $item")
- }
-}
-
-@Sampled
-@Composable
-fun LazyRowForIndexedSample() {
- val items = listOf("A", "B", "C")
- LazyRowForIndexed(items) { index, item ->
- Text("Item at index $index is $item")
- }
-}
\ No newline at end of file
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/MultitouchGestureSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/MultitouchGestureSamples.kt
index a9c5f93..02da81a 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/MultitouchGestureSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/MultitouchGestureSamples.kt
@@ -37,12 +37,10 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.ExperimentalPointerInput
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
-@OptIn(ExperimentalPointerInput::class)
@Composable
@Sampled
fun DetectMultitouchGestures() {
@@ -72,7 +70,6 @@
)
}
-@OptIn(ExperimentalPointerInput::class)
@Composable
@Sampled
fun CalculateRotation() {
@@ -97,7 +94,6 @@
)
}
-@OptIn(ExperimentalPointerInput::class)
@Composable
@Sampled
fun CalculateZoom() {
@@ -121,7 +117,6 @@
)
}
-@OptIn(ExperimentalPointerInput::class)
@Composable
@Sampled
fun CalculatePan() {
@@ -149,7 +144,6 @@
)
}
-@OptIn(ExperimentalPointerInput::class)
@Composable
@Sampled
fun CalculateCentroidSize() {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
index d113774..86c30b6 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
@@ -22,7 +22,6 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focusRequester
import androidx.compose.ui.platform.InspectableValue
@@ -47,7 +46,6 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalFocus::class)
class FocusableTest {
@get:Rule
@@ -98,8 +96,7 @@
@Test
fun focusableTest_focusAcquire() {
- val requester = FocusRequester()
- val otherRequester = FocusRequester()
+ val (requester, otherRequester) = FocusRequester.createRefs()
rule.setContent {
Box {
BasicText(
@@ -139,8 +136,7 @@
@Test
fun focusableTest_interactionState() {
val interactionState = InteractionState()
- val requester = FocusRequester()
- val otherRequester = FocusRequester()
+ val (requester, otherRequester) = FocusRequester.createRefs()
rule.setContent {
Box {
BasicText(
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt
index 591300c..d092269 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt
@@ -58,6 +58,7 @@
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import org.junit.Assert
@@ -302,6 +303,7 @@
}
@Test
+ @LargeTest
fun testImageScalesNonuniformly() {
val imageComposableWidth = imageWidth * 3
val imageComposableHeight = imageHeight * 7
@@ -524,6 +526,7 @@
}
@Test
+ @LargeTest
fun testPainterResourceWithImage() {
val testTag = "testTag"
var imageColor = Color.Black
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
index d4a9fa4..6b3288e 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
@@ -32,6 +32,10 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollSource
+import androidx.compose.ui.gesture.nestedscroll.nestedScroll
import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
@@ -48,6 +52,7 @@
import androidx.compose.ui.test.swipe
import androidx.compose.ui.test.swipeWithVelocity
import androidx.compose.ui.test.up
+import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.milliseconds
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -511,7 +516,6 @@
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
- .testTag(scrollableBoxTag)
.preferredSize(300.dp)
.scrollable(
controller = outerState,
@@ -519,15 +523,89 @@
)
) {
Box(
- modifier = Modifier.preferredSize(300.dp).scrollable(
- controller = innerState,
- orientation = Orientation.Horizontal
- )
+ modifier = Modifier.testTag(scrollableBoxTag)
+ .preferredSize(300.dp)
+ .scrollable(
+ controller = innerState,
+ orientation = Orientation.Horizontal
+ )
)
}
}
}
rule.onNodeWithTag(scrollableBoxTag).performGesture {
+ this.swipeWithVelocity(
+ start = this.center,
+ end = Offset(this.center.x + 200f, this.center.y),
+ duration = 300.milliseconds,
+ endVelocity = 0f
+ )
+ }
+ val lastEqualDrag = rule.runOnIdle {
+ assertThat(innerDrag).isGreaterThan(0f)
+ assertThat(outerDrag).isGreaterThan(0f)
+ // we consumed half delta in child, so exactly half should go to the parent
+ assertThat(outerDrag).isEqualTo(innerDrag)
+ innerDrag
+ }
+ advanceClockWhileAwaitersExist(clock)
+ advanceClockWhileAwaitersExist(clock)
+ rule.runOnIdle {
+ // values should be the same since no fling
+ assertThat(innerDrag).isEqualTo(lastEqualDrag)
+ assertThat(outerDrag).isEqualTo(lastEqualDrag)
+ }
+ }
+
+ @Test
+ @OptIn(ExperimentalTesting::class)
+ fun scrollable_nestedFling() = runBlockingWithManualClock { clock ->
+ var innerDrag = 0f
+ var outerDrag = 0f
+ val animationClock = monotonicFrameAnimationClockOf(coroutineContext, clock)
+ val outerState = ScrollableController(
+ consumeScrollDelta = {
+ outerDrag += it
+ it
+ },
+ flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
+ animationClock = animationClock
+ )
+ val innerState = ScrollableController(
+ consumeScrollDelta = {
+ innerDrag += it / 2
+ it / 2
+ },
+ flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
+ animationClock = animationClock
+ )
+
+ rule.setContent {
+ Box {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .preferredSize(300.dp)
+ .scrollable(
+ controller = outerState,
+ orientation = Orientation.Horizontal
+ )
+ ) {
+ Box(
+ modifier = Modifier
+ .testTag(scrollableBoxTag)
+ .preferredSize(300.dp)
+ .scrollable(
+ controller = innerState,
+ orientation = Orientation.Horizontal
+ )
+ )
+ }
+ }
+ }
+
+ // swipe again with velocity
+ rule.onNodeWithTag(scrollableBoxTag).performGesture {
this.swipe(
start = this.center,
end = Offset(this.center.x + 200f, this.center.y),
@@ -541,16 +619,234 @@
assertThat(outerDrag).isEqualTo(innerDrag)
innerDrag
}
+ // advance clocks, triggering fling
advanceClockWhileAwaitersExist(clock)
advanceClockWhileAwaitersExist(clock)
- // and nothing should change as we don't do nested fling
rule.runOnIdle {
- assertThat(outerDrag).isEqualTo(lastEqualDrag)
+ assertThat(innerDrag).isGreaterThan(lastEqualDrag)
+ assertThat(outerDrag).isGreaterThan(lastEqualDrag)
}
}
@Test
@OptIn(ExperimentalTesting::class)
+ fun scrollable_nestedScrollAbove_respectsPreConsumption() =
+ runBlockingWithManualClock { clock ->
+ var value = 0f
+ var lastReceivedPreScrollAvailable = 0f
+ val preConsumeFraction = 0.7f
+ val animationClock = monotonicFrameAnimationClockOf(coroutineContext, clock)
+ val controller = ScrollableController(
+ consumeScrollDelta = {
+ val expected = lastReceivedPreScrollAvailable * (1 - preConsumeFraction)
+ assertThat(it - expected).isWithin(0.01f)
+ value += it
+ it
+ },
+ flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
+ animationClock = animationClock
+ )
+ val preConsumingParent = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ lastReceivedPreScrollAvailable = available.x
+ return available * preConsumeFraction
+ }
+
+ override fun onPreFling(available: Velocity): Velocity {
+ // consume all velocity
+ return available
+ }
+ }
+
+ rule.setContent {
+ Box {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .preferredSize(300.dp)
+ .nestedScroll(preConsumingParent)
+ ) {
+ Box(
+ modifier = Modifier.preferredSize(300.dp)
+ .testTag(scrollableBoxTag)
+ .scrollable(
+ controller = controller,
+ orientation = Orientation.Horizontal
+ )
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(scrollableBoxTag).performGesture {
+ this.swipe(
+ start = this.center,
+ end = Offset(this.center.x + 200f, this.center.y),
+ duration = 300.milliseconds
+ )
+ }
+
+ val preFlingValue = rule.runOnIdle { value }
+ advanceClockWhileAwaitersExist(clock)
+ advanceClockWhileAwaitersExist(clock)
+ rule.runOnIdle {
+ // if scrollable respects prefling consumption, it should fling 0px since we
+ // preconsume all
+ assertThat(preFlingValue).isEqualTo(value)
+ }
+ }
+
+ @Test
+ @OptIn(ExperimentalTesting::class)
+ fun scrollable_nestedScrollAbove_proxiesPostCycles() =
+ runBlockingWithManualClock { clock ->
+ var value = 0f
+ var expectedLeft = 0f
+ val velocityFlung = 5000f
+ val animationClock = monotonicFrameAnimationClockOf(coroutineContext, clock)
+ val controller = ScrollableController(
+ consumeScrollDelta = {
+ val toConsume = it * 0.345f
+ value += toConsume
+ expectedLeft = it - toConsume
+ toConsume
+ },
+ flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
+ animationClock = animationClock
+ )
+ val parent = object : NestedScrollConnection {
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ // we should get in post scroll as much as left in controller callback
+ assertThat(available.x).isEqualTo(expectedLeft)
+ return available
+ }
+
+ override fun onPostFling(
+ consumed: Velocity,
+ available: Velocity,
+ onFinished: (Velocity) -> Unit
+ ) {
+ assertThat(available.pixelsPerSecond)
+ .isEqualTo(
+ Offset(x = velocityFlung, y = 0f) - consumed.pixelsPerSecond
+ )
+ onFinished.invoke(available)
+ }
+ }
+
+ rule.setContent {
+ Box {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .preferredSize(300.dp)
+ .nestedScroll(parent)
+ ) {
+ Box(
+ modifier = Modifier.preferredSize(300.dp)
+ .testTag(scrollableBoxTag)
+ .scrollable(
+ controller = controller,
+ orientation = Orientation.Horizontal
+ )
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(scrollableBoxTag).performGesture {
+ this.swipeWithVelocity(
+ start = this.center,
+ end = Offset(this.center.x + 500f, this.center.y),
+ duration = 300.milliseconds,
+ endVelocity = velocityFlung
+ )
+ }
+
+ advanceClockWhileAwaitersExist(clock)
+ advanceClockWhileAwaitersExist(clock)
+
+ // all assertions in callback above
+ }
+
+ @Test
+ @OptIn(ExperimentalTesting::class)
+ fun scrollable_nestedScrollBelow_listensDispatches() =
+ runBlockingWithManualClock { clock ->
+ var value = 0f
+ var expectedConsumed = 0f
+ val animationClock = monotonicFrameAnimationClockOf(coroutineContext, clock)
+ val controller = ScrollableController(
+ consumeScrollDelta = {
+ expectedConsumed = it * 0.3f
+ value += expectedConsumed
+ expectedConsumed
+ },
+ flingConfig = FlingConfig(decayAnimation = ExponentialDecay()),
+ animationClock = animationClock
+ )
+ val child = object : NestedScrollConnection {}
+ val dispatcher = NestedScrollDispatcher()
+
+ rule.setContent {
+ Box {
+ Box(
+ modifier = Modifier.preferredSize(300.dp)
+
+ .scrollable(
+ controller = controller,
+ orientation = Orientation.Horizontal
+ )
+ ) {
+ Box(
+ Modifier.preferredSize(200.dp)
+ .testTag(scrollableBoxTag)
+ .nestedScroll(child, dispatcher)
+ )
+ }
+ }
+ }
+
+ val lastValueBeforeFling = rule.runOnIdle {
+ val preScrollConsumed = dispatcher
+ .dispatchPreScroll(Offset(20f, 20f), NestedScrollSource.Drag)
+ // scrollable is not interested in pre scroll
+ assertThat(preScrollConsumed).isEqualTo(Offset.Zero)
+
+ val consumed = dispatcher.dispatchPostScroll(
+ Offset(20f, 20f),
+ Offset(50f, 50f),
+ NestedScrollSource.Drag
+ )
+ assertThat(consumed.x - expectedConsumed).isWithin(0.001f)
+
+ val preFlingConsumed = dispatcher
+ .dispatchPreFling(Velocity(Offset(50f, 50f)))
+ // scrollable won't participate in the pre fling
+ assertThat(preFlingConsumed).isEqualTo(Velocity.Zero)
+
+ dispatcher.dispatchPostFling(
+ Velocity(Offset(1000f, 1000f)),
+ Velocity(Offset(2000f, 2000f))
+ )
+ value
+ }
+
+ advanceClockWhileAwaitersExist(clock)
+ advanceClockWhileAwaitersExist(clock)
+
+ rule.runOnIdle {
+ // catch that scrollable caught our post fling and flung
+ assertThat(value).isGreaterThan(lastValueBeforeFling)
+ }
+ }
+
+ @Test
+ @OptIn(ExperimentalTesting::class)
fun scrollable_interactionState() = runBlocking {
val interactionState = InteractionState()
var total = 0f
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldCursorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldCursorTest.kt
index 847de67..c617a2f 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldCursorTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldCursorTest.kt
@@ -27,7 +27,6 @@
import androidx.compose.testutils.assertPixels
import androidx.compose.testutils.assertShape
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.isFocused
import androidx.compose.ui.focusObserver
import androidx.compose.ui.graphics.Color
@@ -54,7 +53,7 @@
import java.util.concurrent.TimeUnit
@LargeTest
-@OptIn(ExperimentalFocus::class, ExperimentalTesting::class)
+@OptIn(ExperimentalTesting::class)
class TextFieldCursorTest {
@get:Rule
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldFocusTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldFocusTest.kt
index 53591c6..cf8a3c8 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldFocusTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldFocusTest.kt
@@ -22,7 +22,6 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.isFocused
import androidx.compose.ui.focusObserver
@@ -37,7 +36,6 @@
import org.junit.runner.RunWith
@LargeTest
-@OptIn(ExperimentalFocus::class)
@RunWith(AndroidJUnit4::class)
class TextFieldFocusTest {
@get:Rule
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldScrollTest.kt
index 3ee3b91..0009365 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldScrollTest.kt
@@ -57,6 +57,7 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
@@ -247,6 +248,7 @@
}
@Test
+ @LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun testTextField_horizontal_scrolledAndClipped() {
val scrollerPosition = TextFieldScrollerPosition()
@@ -294,6 +296,7 @@
}
@Test
+ @LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun testTextField_vertical_scrolledAndClipped() {
val scrollerPosition = TextFieldScrollerPosition()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldTest.kt
index 1c589bf..d231d22 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldTest.kt
@@ -37,7 +37,6 @@
import androidx.compose.runtime.setValue
import androidx.compose.testutils.assertShape
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.isFocused
import androidx.compose.ui.focusObserver
import androidx.compose.ui.graphics.Color
@@ -97,10 +96,7 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
-@OptIn(
- ExperimentalFocus::class,
- ExperimentalFoundationApi::class
-)
+@OptIn(ExperimentalFoundationApi::class)
class TextFieldTest {
@get:Rule
val rule = createComposeRule()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyArrangementsTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyArrangementsTest.kt
new file mode 100644
index 0000000..994dba7
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyArrangementsTest.kt
@@ -0,0 +1,377 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.InternalLayoutApi
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Providers
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.AmbientLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@OptIn(InternalLayoutApi::class)
+class LazyArrangementsTest {
+
+ private val ContainerTag = "ContainerTag"
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private var itemSize: Dp = Dp.Infinity
+ private var containerSize: Dp = Dp.Infinity
+
+ @Before
+ fun before() {
+ with(rule.density) {
+ itemSize = 50.toDp()
+ }
+ containerSize = itemSize * 5
+ }
+
+ // cases when we have not enough items to fill min constraints:
+
+ @Test
+ fun column_defaultArrangementIsTop() {
+ rule.setContent {
+ LazyColumn(
+ modifier = Modifier.size(containerSize)
+ ) {
+ items((0..1).toList()) {
+ Box(Modifier.size(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ assertArrangementForTwoItems(Arrangement.Top)
+ }
+
+ @Test
+ fun column_centerArrangement() {
+ composeColumnWith(Arrangement.Center)
+ assertArrangementForTwoItems(Arrangement.Center)
+ }
+
+ @Test
+ fun column_bottomArrangement() {
+ composeColumnWith(Arrangement.Bottom)
+ assertArrangementForTwoItems(Arrangement.Bottom)
+ }
+
+ @Test
+ fun column_spacedArrangementNotFillingViewport() {
+ val arrangement = Arrangement.spacedBy(10.dp)
+ composeColumnWith(arrangement)
+ assertArrangementForTwoItems(arrangement)
+ }
+
+ @Test
+ fun row_defaultArrangementIsStart() {
+ rule.setContent {
+ LazyRow(
+ modifier = Modifier.size(containerSize)
+ ) {
+ items((0..1).toList()) {
+ Box(Modifier.size(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ assertArrangementForTwoItems(Arrangement.Start, LayoutDirection.Ltr)
+ }
+
+ @Test
+ fun row_centerArrangement() {
+ composeRowWith(Arrangement.Center, LayoutDirection.Ltr)
+ assertArrangementForTwoItems(Arrangement.Center, LayoutDirection.Ltr)
+ }
+
+ @Test
+ fun row_endArrangement() {
+ composeRowWith(Arrangement.End, LayoutDirection.Ltr)
+ assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Ltr)
+ }
+
+ @Test
+ fun row_spacedArrangementNotFillingViewport() {
+ val arrangement = Arrangement.spacedBy(10.dp)
+ composeRowWith(arrangement, LayoutDirection.Ltr)
+ assertArrangementForTwoItems(arrangement, LayoutDirection.Ltr)
+ }
+
+ @Test
+ fun row_rtl_startArrangement() {
+ composeRowWith(Arrangement.Center, LayoutDirection.Rtl)
+ assertArrangementForTwoItems(Arrangement.Center, LayoutDirection.Rtl)
+ }
+
+ @Test
+ fun row_rtl_endArrangement() {
+ composeRowWith(Arrangement.End, LayoutDirection.Rtl)
+ assertArrangementForTwoItems(Arrangement.End, LayoutDirection.Rtl)
+ }
+
+ @Test
+ fun row_rtl_spacedArrangementNotFillingViewport() {
+ val arrangement = Arrangement.spacedBy(10.dp)
+ composeRowWith(arrangement, LayoutDirection.Rtl)
+ assertArrangementForTwoItems(arrangement, LayoutDirection.Rtl)
+ }
+
+ // wrap content and spacing
+
+ @Test
+ fun column_spacing_affects_wrap_content() {
+ rule.setContent {
+ LazyColumn(
+ verticalArrangement = Arrangement.spacedBy(itemSize),
+ modifier = Modifier.testTag(ContainerTag)
+ ) {
+ items((0..1).toList()) {
+ Box(Modifier.size(itemSize))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ContainerTag)
+ .assertWidthIsEqualTo(itemSize)
+ .assertHeightIsEqualTo(itemSize * 3)
+ }
+
+ @Test
+ fun row_spacing_affects_wrap_content() {
+ rule.setContent {
+ LazyRow(
+ horizontalArrangement = Arrangement.spacedBy(itemSize),
+ modifier = Modifier.testTag(ContainerTag)
+ ) {
+ items((0..1).toList()) {
+ Box(Modifier.size(itemSize))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ContainerTag)
+ .assertWidthIsEqualTo(itemSize * 3)
+ .assertHeightIsEqualTo(itemSize)
+ }
+
+ // spacing added when we have enough items to fill the viewport
+
+ @Test
+ fun column_spacing_scrolledToTheTop() {
+ rule.setContent {
+ LazyColumn(
+ verticalArrangement = Arrangement.spacedBy(itemSize),
+ modifier = Modifier.size(itemSize * 3.5f)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.size(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize * 2)
+ }
+
+ @Test
+ fun column_spacing_scrolledToTheBottom() {
+ rule.setContent {
+ LazyColumn(
+ verticalArrangement = Arrangement.spacedBy(itemSize),
+ modifier = Modifier.size(itemSize * 3.5f).testTag(ContainerTag)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.size(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ContainerTag)
+ .scrollBy(y = itemSize * 2, density = rule.density)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(itemSize * 0.5f)
+
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize * 2.5f)
+ }
+
+ @Test
+ fun row_spacing_scrolledToTheStart() {
+ rule.setContent {
+ LazyRow(
+ horizontalArrangement = Arrangement.spacedBy(itemSize),
+ modifier = Modifier.size(itemSize * 3.5f)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.size(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 2)
+ }
+
+ @Test
+ fun row_spacing_scrolledToTheEnd() {
+ rule.setContent {
+ LazyRow(
+ horizontalArrangement = Arrangement.spacedBy(itemSize),
+ modifier = Modifier.size(itemSize * 3.5f).testTag(ContainerTag)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.size(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ContainerTag)
+ .scrollBy(x = itemSize * 2, density = rule.density)
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 0.5f)
+
+ rule.onNodeWithTag("2")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 2.5f)
+ }
+
+ // with reverseLayout == true
+
+ @Test
+ fun column_defaultArrangementIsBottomWithReverseLayout() {
+ rule.setContent {
+ LazyColumn(
+ reverseLayout = true,
+ modifier = Modifier.size(containerSize)
+ ) {
+ items((0..1).toList()) {
+ Box(Modifier.size(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ assertArrangementForTwoItems(Arrangement.Bottom, reversedItemsOrder = true)
+ }
+
+ @Test
+ fun row_defaultArrangementIsEndWithReverseLayout() {
+ rule.setContent {
+ LazyRow(
+ reverseLayout = true,
+ modifier = Modifier.size(containerSize)
+ ) {
+ items((0..1).toList()) {
+ Box(Modifier.size(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+
+ assertArrangementForTwoItems(
+ Arrangement.End, LayoutDirection.Ltr, reversedItemsOrder = true
+ )
+ }
+
+ fun composeColumnWith(arrangement: Arrangement.Vertical) {
+ rule.setContent {
+ LazyColumn(
+ verticalArrangement = arrangement,
+ modifier = Modifier.size(containerSize)
+ ) {
+ items((0..1).toList()) {
+ Box(Modifier.size(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+ }
+
+ fun composeRowWith(arrangement: Arrangement.Horizontal, layoutDirection: LayoutDirection) {
+ rule.setContent {
+ Providers(AmbientLayoutDirection provides layoutDirection) {
+ LazyRow(
+ horizontalArrangement = arrangement,
+ modifier = Modifier.size(containerSize)
+ ) {
+ items((0..1).toList()) {
+ Box(Modifier.size(itemSize).testTag(it.toString()))
+ }
+ }
+ }
+ }
+ }
+
+ fun assertArrangementForTwoItems(
+ arrangement: Arrangement.Vertical,
+ reversedItemsOrder: Boolean = false
+ ) {
+ with(rule.density) {
+ val sizes = IntArray(2) { itemSize.toIntPx() }
+ val outPositions = IntArray(2) { 0 }
+ arrangement.arrange(containerSize.toIntPx(), sizes, this, outPositions)
+
+ outPositions.forEachIndexed { index, position ->
+ val realIndex = if (reversedItemsOrder) if (index == 0) 1 else 0 else index
+ rule.onNodeWithTag("$realIndex")
+ .assertTopPositionInRootIsEqualTo(position.toDp())
+ }
+ }
+ }
+
+ fun assertArrangementForTwoItems(
+ arrangement: Arrangement.Horizontal,
+ layoutDirection: LayoutDirection,
+ reversedItemsOrder: Boolean = false
+ ) {
+ with(rule.density) {
+ val sizes = IntArray(2) { itemSize.toIntPx() }
+ val outPositions = IntArray(2) { 0 }
+ arrangement.arrange(containerSize.toIntPx(), sizes, layoutDirection, this, outPositions)
+
+ outPositions.forEachIndexed { index, position ->
+ val realIndex = if (reversedItemsOrder) if (index == 0) 1 else 0 else index
+ val expectedPosition = if (layoutDirection == LayoutDirection.Ltr) {
+ position.toDp()
+ } else {
+ containerSize - position.toDp() - itemSize
+ }
+ rule.onNodeWithTag("$realIndex")
+ .assertLeftPositionInRootIsEqualTo(expectedPosition)
+ }
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnForTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnForTest.kt
deleted file mode 100644
index bbd257b..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnForTest.kt
+++ /dev/null
@@ -1,1136 +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.compose.foundation.lazy
-
-import androidx.compose.animation.core.ExponentialDecay
-import androidx.compose.animation.core.ManualAnimationClock
-import androidx.compose.animation.core.snap
-import androidx.compose.foundation.animation.FlingConfig
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.preferredHeight
-import androidx.compose.foundation.layout.preferredSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.text.BasicText
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.onCommit
-import androidx.compose.runtime.onDispose
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.TouchSlop
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.assertCountEquals
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsEqualTo
-import androidx.compose.ui.test.assertIsNotDisplayed
-import androidx.compose.ui.test.assertPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.center
-import androidx.compose.ui.test.click
-import androidx.compose.ui.test.getUnclippedBoundsInRoot
-import androidx.compose.ui.test.junit4.StateRestorationTester
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onChildren
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.performGesture
-import androidx.compose.ui.test.swipeUp
-import androidx.compose.ui.test.swipeWithVelocity
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import com.google.common.collect.Range
-import com.google.common.truth.IntegerSubject
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlinx.coroutines.runBlocking
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-class LazyColumnForTest {
- private val LazyColumnForTag = "TestLazyColumnFor"
-
- @get:Rule
- val rule = createComposeRule()
-
- @Test
- fun compositionsAreDisposed_whenNodesAreScrolledOff() {
- var composed: Boolean
- var disposed = false
- // Ten 31dp spacers in a 300dp list
- val latch = CountDownLatch(10)
- // Make it long enough that it's _definitely_ taller than the screen
- val data = (1..50).toList()
-
- rule.setContent {
- // Fixed height to eliminate device size as a factor
- Box(Modifier.testTag(LazyColumnForTag).preferredHeight(300.dp)) {
- LazyColumnFor(items = data, modifier = Modifier.fillMaxSize()) {
- onCommit {
- composed = true
- // Signal when everything is done composing
- latch.countDown()
- onDispose {
- disposed = true
- }
- }
-
- // There will be 10 of these in the 300dp box
- Spacer(Modifier.preferredHeight(31.dp))
- }
- }
- }
-
- latch.await()
- composed = false
-
- assertWithMessage("Compositions were disposed before we did any scrolling")
- .that(disposed).isFalse()
-
- // Mostly a validity check, this is not part of the behavior under test
- assertWithMessage("Additional composition occurred for no apparent reason")
- .that(composed).isFalse()
-
- rule.onNodeWithTag(LazyColumnForTag)
- .performGesture { swipeUp() }
-
- rule.waitForIdle()
-
- assertWithMessage("No additional items were composed after scroll, scroll didn't work")
- .that(composed).isTrue()
-
- // We may need to modify this test once we prefetch/cache items outside the viewport
- assertWithMessage(
- "No compositions were disposed after scrolling, compositions were leaked"
- ).that(disposed).isTrue()
- }
-
- @Test
- fun compositionsAreDisposed_whenDataIsChanged() {
- var composed = 0
- var disposals = 0
- val data1 = (1..3).toList()
- val data2 = (4..5).toList() // smaller, to ensure removal is handled properly
-
- var part2 by mutableStateOf(false)
-
- rule.setContent {
- LazyColumnFor(
- items = if (!part2) data1 else data2,
- modifier = Modifier.testTag(LazyColumnForTag).fillMaxSize()
- ) {
- onCommit {
- composed++
- onDispose {
- disposals++
- }
- }
-
- Spacer(Modifier.height(50.dp))
- }
- }
-
- rule.runOnIdle {
- assertWithMessage("Not all items were composed")
- .that(composed).isEqualTo(data1.size)
- composed = 0
-
- part2 = true
- }
-
- rule.runOnIdle {
- assertWithMessage(
- "No additional items were composed after data change, something didn't work"
- ).that(composed).isEqualTo(data2.size)
-
- // We may need to modify this test once we prefetch/cache items outside the viewport
- assertWithMessage(
- "Not enough compositions were disposed after scrolling, compositions were leaked"
- ).that(disposals).isEqualTo(data1.size)
- }
- }
-
- @Test
- fun compositionsAreDisposed_whenAdapterListIsDisposed() {
- var emitAdapterList by mutableStateOf(true)
- var disposeCalledOnFirstItem = false
- var disposeCalledOnSecondItem = false
-
- rule.setContent {
- if (emitAdapterList) {
- LazyColumnFor(
- items = listOf(0, 1),
- modifier = Modifier.fillMaxSize()
- ) {
- Box(Modifier.size(100.dp))
- onDispose {
- if (it == 1) {
- disposeCalledOnFirstItem = true
- } else {
- disposeCalledOnSecondItem = true
- }
- }
- }
- }
- }
-
- rule.runOnIdle {
- assertWithMessage("First item is not immediately disposed")
- .that(disposeCalledOnFirstItem).isFalse()
- assertWithMessage("Second item is not immediately disposed")
- .that(disposeCalledOnFirstItem).isFalse()
- emitAdapterList = false
- }
-
- rule.runOnIdle {
- assertWithMessage("First item is correctly disposed")
- .that(disposeCalledOnFirstItem).isTrue()
- assertWithMessage("Second item is correctly disposed")
- .that(disposeCalledOnSecondItem).isTrue()
- }
- }
-
- @Test
- fun removeItemsTest() {
- val startingNumItems = 3
- var numItems = startingNumItems
- var numItemsModel by mutableStateOf(numItems)
- val tag = "List"
- rule.setContent {
- LazyColumnFor((1..numItemsModel).toList(), modifier = Modifier.testTag(tag)) {
- BasicText("$it")
- }
- }
-
- while (numItems >= 0) {
- // Confirm the number of children to ensure there are no extra items
- rule.onNodeWithTag(tag)
- .onChildren()
- .assertCountEquals(numItems)
-
- // Confirm the children's content
- for (i in 1..3) {
- rule.onNodeWithText("$i").apply {
- if (i <= numItems) {
- assertExists()
- } else {
- assertDoesNotExist()
- }
- }
- }
- numItems--
- if (numItems >= 0) {
- // Don't set the model to -1
- rule.runOnIdle { numItemsModel = numItems }
- }
- }
- }
-
- @Test
- fun changingDataTest() {
- val dataLists = listOf(
- (1..3).toList(),
- (4..8).toList(),
- (3..4).toList()
- )
- var dataModel by mutableStateOf(dataLists[0])
- val tag = "List"
- rule.setContent {
- LazyColumnFor(dataModel, modifier = Modifier.testTag(tag)) {
- BasicText("$it")
- }
- }
-
- for (data in dataLists) {
- rule.runOnIdle { dataModel = data }
-
- // Confirm the number of children to ensure there are no extra items
- val numItems = data.size
- rule.onNodeWithTag(tag)
- .onChildren()
- .assertCountEquals(numItems)
-
- // Confirm the children's content
- for (item in data) {
- rule.onNodeWithText("$item").assertExists()
- }
- }
- }
-
- @Test
- fun whenItemsAreInitiallyCreatedWith0SizeWeCanScrollWhenTheyExpanded() {
- val thirdTag = "third"
- val items = (1..3).toList()
- var thirdHasSize by mutableStateOf(false)
-
- rule.setContent {
- LazyColumnFor(
- items = items,
- modifier = Modifier.fillMaxWidth()
- .preferredHeight(100.dp)
- .testTag(LazyColumnForTag)
- ) {
- if (it == 3) {
- Spacer(
- Modifier.testTag(thirdTag)
- .fillParentMaxWidth()
- .preferredHeight(if (thirdHasSize) 60.dp else 0.dp)
- )
- } else {
- Spacer(Modifier.fillParentMaxWidth().preferredHeight(60.dp))
- }
- }
- }
-
- rule.onNodeWithTag(LazyColumnForTag)
- .scrollBy(y = 21.dp, density = rule.density)
-
- rule.onNodeWithTag(thirdTag)
- .assertExists()
- .assertIsNotDisplayed()
-
- rule.runOnIdle {
- thirdHasSize = true
- }
-
- rule.waitForIdle()
-
- rule.onNodeWithTag(LazyColumnForTag)
- .scrollBy(y = 10.dp, density = rule.density)
-
- rule.onNodeWithTag(thirdTag)
- .assertIsDisplayed()
- }
-
- @Test
- fun lazyColumnWrapsContent() = with(rule.density) {
- val itemInsideLazyColumn = "itemInsideLazyColumn"
- val itemOutsideLazyColumn = "itemOutsideLazyColumn"
- var sameSizeItems by mutableStateOf(true)
-
- rule.setContent {
- Row {
- LazyColumnFor(
- items = listOf(1, 2),
- modifier = Modifier.testTag(LazyColumnForTag)
- ) {
- if (it == 1) {
- Spacer(Modifier.preferredSize(50.dp).testTag(itemInsideLazyColumn))
- } else {
- Spacer(Modifier.preferredSize(if (sameSizeItems) 50.dp else 70.dp))
- }
- }
- Spacer(Modifier.preferredSize(50.dp).testTag(itemOutsideLazyColumn))
- }
- }
-
- rule.onNodeWithTag(itemInsideLazyColumn)
- .assertIsDisplayed()
-
- rule.onNodeWithTag(itemOutsideLazyColumn)
- .assertIsDisplayed()
-
- var lazyColumnBounds = rule.onNodeWithTag(LazyColumnForTag)
- .getUnclippedBoundsInRoot()
-
- assertThat(lazyColumnBounds.left.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
- assertThat(lazyColumnBounds.right.toIntPx()).isWithin1PixelFrom(50.dp.toIntPx())
- assertThat(lazyColumnBounds.top.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
- assertThat(lazyColumnBounds.bottom.toIntPx()).isWithin1PixelFrom(100.dp.toIntPx())
-
- rule.runOnIdle {
- sameSizeItems = false
- }
-
- rule.waitForIdle()
-
- rule.onNodeWithTag(itemInsideLazyColumn)
- .assertIsDisplayed()
-
- rule.onNodeWithTag(itemOutsideLazyColumn)
- .assertIsDisplayed()
-
- lazyColumnBounds = rule.onNodeWithTag(LazyColumnForTag)
- .getUnclippedBoundsInRoot()
-
- assertThat(lazyColumnBounds.left.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
- assertThat(lazyColumnBounds.right.toIntPx()).isWithin1PixelFrom(70.dp.toIntPx())
- assertThat(lazyColumnBounds.top.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
- assertThat(lazyColumnBounds.bottom.toIntPx()).isWithin1PixelFrom(120.dp.toIntPx())
- }
-
- private val firstItemTag = "firstItemTag"
- private val secondItemTag = "secondItemTag"
-
- private fun prepareLazyColumnsItemsAlignment(horizontalGravity: Alignment.Horizontal) {
- rule.setContent {
- LazyColumnFor(
- items = listOf(1, 2),
- modifier = Modifier.testTag(LazyColumnForTag).width(100.dp),
- horizontalAlignment = horizontalGravity
- ) {
- if (it == 1) {
- Spacer(Modifier.preferredSize(50.dp).testTag(firstItemTag))
- } else {
- Spacer(Modifier.preferredSize(70.dp).testTag(secondItemTag))
- }
- }
- }
-
- rule.onNodeWithTag(firstItemTag)
- .assertIsDisplayed()
-
- rule.onNodeWithTag(secondItemTag)
- .assertIsDisplayed()
-
- val lazyColumnBounds = rule.onNodeWithTag(LazyColumnForTag)
- .getUnclippedBoundsInRoot()
-
- with(rule.density) {
- // Verify the width of the column
- assertThat(lazyColumnBounds.left.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
- assertThat(lazyColumnBounds.right.toIntPx()).isWithin1PixelFrom(100.dp.toIntPx())
- }
- }
-
- @Test
- fun lazyColumnAlignmentCenterHorizontally() {
- prepareLazyColumnsItemsAlignment(Alignment.CenterHorizontally)
-
- rule.onNodeWithTag(firstItemTag)
- .assertPositionInRootIsEqualTo(25.dp, 0.dp)
-
- rule.onNodeWithTag(secondItemTag)
- .assertPositionInRootIsEqualTo(15.dp, 50.dp)
- }
-
- @Test
- fun lazyColumnAlignmentStart() {
- prepareLazyColumnsItemsAlignment(Alignment.Start)
-
- rule.onNodeWithTag(firstItemTag)
- .assertPositionInRootIsEqualTo(0.dp, 0.dp)
-
- rule.onNodeWithTag(secondItemTag)
- .assertPositionInRootIsEqualTo(0.dp, 50.dp)
- }
-
- @Test
- fun lazyColumnAlignmentEnd() {
- prepareLazyColumnsItemsAlignment(Alignment.End)
-
- rule.onNodeWithTag(firstItemTag)
- .assertPositionInRootIsEqualTo(50.dp, 0.dp)
-
- rule.onNodeWithTag(secondItemTag)
- .assertPositionInRootIsEqualTo(30.dp, 50.dp)
- }
-
- @Test
- fun itemFillingParentWidth() {
- rule.setContent {
- LazyColumnFor(
- items = listOf(0),
- modifier = Modifier.size(width = 100.dp, height = 150.dp)
- ) {
- Spacer(Modifier.fillParentMaxWidth().height(50.dp).testTag(firstItemTag))
- }
- }
-
- rule.onNodeWithTag(firstItemTag)
- .assertWidthIsEqualTo(100.dp)
- .assertHeightIsEqualTo(50.dp)
- }
-
- @Test
- fun itemFillingParentHeight() {
- rule.setContent {
- LazyColumnFor(
- items = listOf(0),
- modifier = Modifier.size(width = 100.dp, height = 150.dp)
- ) {
- Spacer(Modifier.width(50.dp).fillParentMaxHeight().testTag(firstItemTag))
- }
- }
-
- rule.onNodeWithTag(firstItemTag)
- .assertWidthIsEqualTo(50.dp)
- .assertHeightIsEqualTo(150.dp)
- }
-
- @Test
- fun itemFillingParentSize() {
- rule.setContent {
- LazyColumnFor(
- items = listOf(0),
- modifier = Modifier.size(width = 100.dp, height = 150.dp)
- ) {
- Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
- }
- }
-
- rule.onNodeWithTag(firstItemTag)
- .assertWidthIsEqualTo(100.dp)
- .assertHeightIsEqualTo(150.dp)
- }
-
- @Test
- fun itemFillingParentWidthFraction() {
- rule.setContent {
- LazyColumnFor(
- items = listOf(0),
- modifier = Modifier.size(width = 100.dp, height = 150.dp)
- ) {
- Spacer(Modifier.fillParentMaxWidth(0.6f).height(50.dp).testTag(firstItemTag))
- }
- }
-
- rule.onNodeWithTag(firstItemTag)
- .assertWidthIsEqualTo(60.dp)
- .assertHeightIsEqualTo(50.dp)
- }
-
- @Test
- fun itemFillingParentHeightFraction() {
- rule.setContent {
- LazyColumnFor(
- items = listOf(0),
- modifier = Modifier.size(width = 100.dp, height = 150.dp)
- ) {
- Spacer(Modifier.width(50.dp).fillParentMaxHeight(0.2f).testTag(firstItemTag))
- }
- }
-
- rule.onNodeWithTag(firstItemTag)
- .assertWidthIsEqualTo(50.dp)
- .assertHeightIsEqualTo(30.dp)
- }
-
- @Test
- fun itemFillingParentSizeFraction() {
- rule.setContent {
- LazyColumnFor(
- items = listOf(0),
- modifier = Modifier.size(width = 100.dp, height = 150.dp)
- ) {
- Spacer(Modifier.fillParentMaxSize(0.1f).testTag(firstItemTag))
- }
- }
-
- rule.onNodeWithTag(firstItemTag)
- .assertWidthIsEqualTo(10.dp)
- .assertHeightIsEqualTo(15.dp)
- }
-
- @Test
- fun itemFillingParentSizeParentResized() {
- var parentSize by mutableStateOf(100.dp)
- rule.setContent {
- LazyColumnFor(
- items = listOf(0),
- modifier = Modifier.size(parentSize)
- ) {
- Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
- }
- }
-
- rule.runOnIdle {
- parentSize = 150.dp
- }
-
- rule.onNodeWithTag(firstItemTag)
- .assertWidthIsEqualTo(150.dp)
- .assertHeightIsEqualTo(150.dp)
- }
-
- @Test
- fun whenNotAnymoreAvailableItemWasDisplayed() {
- var items by mutableStateOf((1..30).toList())
- rule.setContent {
- LazyColumnFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyColumnForTag)
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- // after scroll we will display items 16-20
- rule.onNodeWithTag(LazyColumnForTag)
- .scrollBy(y = 300.dp, density = rule.density)
-
- rule.runOnIdle {
- items = (1..10).toList()
- }
-
- // there is no item 16 anymore so we will just display the last items 6-10
- rule.onNodeWithTag("6")
- .assertTopPositionIsAlmost(0.dp)
- }
-
- @Test
- fun whenFewDisplayedItemsWereRemoved() {
- var items by mutableStateOf((1..10).toList())
- rule.setContent {
- LazyColumnFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyColumnForTag)
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- // after scroll we will display items 6-10
- rule.onNodeWithTag(LazyColumnForTag)
- .scrollBy(y = 100.dp, density = rule.density)
-
- rule.runOnIdle {
- items = (1..8).toList()
- }
-
- // there are no more items 9 and 10, so we have to scroll back
- rule.onNodeWithTag("4")
- .assertTopPositionIsAlmost(0.dp)
- }
-
- @Test
- fun whenItemsBecameEmpty() {
- var items by mutableStateOf((1..10).toList())
- rule.setContent {
- LazyColumnFor(
- items = items,
- modifier = Modifier.sizeIn(maxHeight = 100.dp).testTag(LazyColumnForTag)
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- // after scroll we will display items 2-6
- rule.onNodeWithTag(LazyColumnForTag)
- .scrollBy(y = 20.dp, density = rule.density)
-
- rule.runOnIdle {
- items = emptyList()
- }
-
- // there are no more items so the LazyColumn is zero sized
- rule.onNodeWithTag(LazyColumnForTag)
- .assertWidthIsEqualTo(0.dp)
- .assertHeightIsEqualTo(0.dp)
-
- // and has no children
- rule.onNodeWithTag("1")
- .assertDoesNotExist()
- rule.onNodeWithTag("2")
- .assertDoesNotExist()
- }
-
- @Test
- fun scrollBackAndForth() {
- val items by mutableStateOf((1..20).toList())
- rule.setContent {
- LazyColumnFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyColumnForTag)
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- // after scroll we will display items 6-10
- rule.onNodeWithTag(LazyColumnForTag)
- .scrollBy(y = 100.dp, density = rule.density)
-
- // and scroll back
- rule.onNodeWithTag(LazyColumnForTag)
- .scrollBy(y = (-100).dp, density = rule.density)
-
- rule.onNodeWithTag("1")
- .assertTopPositionIsAlmost(0.dp)
- }
-
- @Test
- fun tryToScrollBackwardWhenAlreadyOnTop() {
- val items by mutableStateOf((1..20).toList())
- rule.setContent {
- LazyColumnFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyColumnForTag)
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- // we already displaying the first item, so this should do nothing
- rule.onNodeWithTag(LazyColumnForTag)
- .scrollBy(y = (-50).dp, density = rule.density)
-
- rule.onNodeWithTag("1")
- .assertTopPositionIsAlmost(0.dp)
- rule.onNodeWithTag("5")
- .assertTopPositionIsAlmost(80.dp)
- }
-
- @Test
- fun contentOfNotStableItemsIsNotRecomposedDuringScroll() {
- val items = listOf(NotStable(1), NotStable(2))
- var firstItemRecomposed = 0
- var secondItemRecomposed = 0
- rule.setContent {
- LazyColumnFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyColumnForTag)
- ) {
- if (it.count == 1) {
- firstItemRecomposed++
- } else {
- secondItemRecomposed++
- }
- Spacer(Modifier.size(75.dp))
- }
- }
-
- rule.runOnIdle {
- assertThat(firstItemRecomposed).isEqualTo(1)
- assertThat(secondItemRecomposed).isEqualTo(1)
- }
-
- rule.onNodeWithTag(LazyColumnForTag)
- .scrollBy(y = (50).dp, density = rule.density)
-
- rule.runOnIdle {
- assertThat(firstItemRecomposed).isEqualTo(1)
- assertThat(secondItemRecomposed).isEqualTo(1)
- }
- }
-
- @Test
- fun onlyOneMeasurePassForScrollEvent() {
- val items by mutableStateOf((1..20).toList())
- lateinit var state: LazyListState
- rule.setContent {
- state = rememberLazyListState()
- LazyColumnFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyColumnForTag),
- state = state
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- val initialMeasurePasses = state.numMeasurePasses
-
- rule.runOnIdle {
- with(rule.density) {
- state.onScroll(-110.dp.toPx())
- }
- }
-
- rule.waitForIdle()
-
- assertThat(state.numMeasurePasses).isEqualTo(initialMeasurePasses + 1)
- }
-
- @Test
- fun stateUpdatedAfterScroll() {
- val items by mutableStateOf((1..20).toList())
- lateinit var state: LazyListState
- rule.setContent {
- state = rememberLazyListState()
- LazyColumnFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyColumnForTag),
- state = state
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
-
- rule.onNodeWithTag(LazyColumnForTag)
- .scrollBy(y = 30.dp, density = rule.density)
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(1)
-
- with(rule.density) {
- // TODO(b/169232491): test scrolling doesn't appear to be scrolling exactly the right
- // number of pixels
- val expectedOffset = 10.dp.toIntPx()
- val tolerance = 2.dp.toIntPx()
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset, tolerance)
- }
- }
- }
-
- @Test
- fun isAnimationRunningUpdate() {
- val items by mutableStateOf((1..20).toList())
- val clock = ManualAnimationClock(0L)
- val state = LazyListState(
- flingConfig = FlingConfig(ExponentialDecay()),
- animationClock = clock
- )
- rule.setContent {
- LazyColumnFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyColumnForTag),
- state = state
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.isAnimationRunning).isEqualTo(false)
- }
-
- rule.onNodeWithTag(LazyColumnForTag)
- .performGesture { swipeUp() }
-
- rule.runOnIdle {
- clock.clockTimeMillis += 100
- assertThat(state.firstVisibleItemIndex).isNotEqualTo(0)
- assertThat(state.isAnimationRunning).isEqualTo(true)
- }
-
- // TODO (jelle): this should be down, and not click to be 100% fair
- rule.onNodeWithTag(LazyColumnForTag)
- .performGesture { click() }
-
- rule.runOnIdle {
- assertThat(state.isAnimationRunning).isEqualTo(false)
- }
- }
-
- @Test
- fun stateUpdatedAfterScrollWithinTheSameItem() {
- val items by mutableStateOf((1..20).toList())
- lateinit var state: LazyListState
- rule.setContent {
- state = rememberLazyListState()
- LazyColumnFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyColumnForTag),
- state = state
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- rule.onNodeWithTag(LazyColumnForTag)
- .scrollBy(y = 10.dp, density = rule.density)
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- with(rule.density) {
- val expectedOffset = 10.dp.toIntPx()
- val tolerance = 2.dp.toIntPx()
- assertThat(state.firstVisibleItemScrollOffset)
- .isEqualTo(expectedOffset, tolerance)
- }
- }
- }
-
- @Test
- fun initialScrollIsApplied() {
- val items by mutableStateOf((0..20).toList())
- lateinit var state: LazyListState
- val expectedOffset = with(rule.density) { 10.dp.toIntPx() }
- rule.setContent {
- state = rememberLazyListState(2, expectedOffset)
- LazyColumnFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyColumnForTag),
- state = state
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(2)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset)
- }
-
- rule.onNodeWithTag("2")
- .assertTopPositionInRootIsEqualTo((-10).dp)
- }
-
- @Test
- fun stateIsRestored() {
- val restorationTester = StateRestorationTester(rule)
- val items by mutableStateOf((1..20).toList())
- var state: LazyListState? = null
- restorationTester.setContent {
- state = rememberLazyListState()
- LazyColumnFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyColumnForTag),
- state = state!!
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- rule.onNodeWithTag(LazyColumnForTag)
- .scrollBy(y = 30.dp, density = rule.density)
-
- val (index, scrollOffset) = rule.runOnIdle {
- state!!.firstVisibleItemIndex to state!!.firstVisibleItemScrollOffset
- }
-
- state = null
-
- restorationTester.emulateSavedInstanceStateRestore()
-
- rule.runOnIdle {
- assertThat(state!!.firstVisibleItemIndex).isEqualTo(index)
- assertThat(state!!.firstVisibleItemScrollOffset).isEqualTo(scrollOffset)
- }
- }
-
- @Test
- fun scroll_makeListSmaller_scroll() {
- var items by mutableStateOf((1..100).toList())
- rule.setContent {
- LazyColumnFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyColumnForTag)
- ) {
- Spacer(Modifier.size(10.dp).testTag("$it"))
- }
- }
-
- rule.onNodeWithTag(LazyColumnForTag)
- .scrollBy(y = 300.dp, density = rule.density)
-
- rule.runOnIdle {
- items = (1..11).toList()
- }
-
- // try to scroll after the data set has been updated. this was causing a crash previously
- rule.onNodeWithTag(LazyColumnForTag)
- .scrollBy(y = (-10).dp, density = rule.density)
-
- rule.onNodeWithTag("1")
- .assertIsDisplayed()
- }
-
- @Test
- fun snapToItemIndex() {
- val items by mutableStateOf((1..20).toList())
- lateinit var state: LazyListState
- rule.setContent {
- state = rememberLazyListState()
- LazyColumnFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyColumnForTag),
- state = state
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- rule.runOnIdle {
- runBlocking {
- state.snapToItemIndex(3, 10)
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(3)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
- }
- }
-
- @Test
- fun itemsAreNotRedrawnDuringScroll() {
- val items = (0..20).toList()
- val redrawCount = Array(6) { 0 }
- rule.setContent {
- LazyColumnFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyColumnForTag)
- ) {
- Spacer(
- Modifier.size(20.dp)
- .drawBehind { redrawCount[it]++ }
- )
- }
- }
-
- rule.onNodeWithTag(LazyColumnForTag)
- .scrollBy(y = 10.dp, density = rule.density)
-
- rule.runOnIdle {
- redrawCount.forEachIndexed { index, i ->
- assertWithMessage("Item with index $index was redrawn $i times")
- .that(i).isEqualTo(1)
- }
- }
- }
-
- @Test
- fun itemInvalidationIsNotCausingAnotherItemToRedraw() {
- val items = (0..1).toList()
- val redrawCount = Array(2) { 0 }
- var stateUsedInDrawScope by mutableStateOf(false)
- rule.setContent {
- LazyColumnFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyColumnForTag)
- ) {
- Spacer(
- Modifier.size(50.dp)
- .drawBehind {
- redrawCount[it]++
- if (it == 1) {
- stateUsedInDrawScope.hashCode()
- }
- }
- )
- }
- }
-
- rule.runOnIdle {
- stateUsedInDrawScope = true
- }
-
- rule.runOnIdle {
- assertWithMessage("First items is not expected to be redrawn")
- .that(redrawCount[0]).isEqualTo(1)
- assertWithMessage("Second items is expected to be redrawn")
- .that(redrawCount[1]).isEqualTo(2)
- }
- }
-
- @Test
- fun notVisibleAnymoreItemNotAffectingCrossAxisSize() {
- val items = (0..1).toList()
- val itemSize = with(rule.density) { 30.toDp() }
- val itemSizeMinusOne = with(rule.density) { 29.toDp() }
- lateinit var state: LazyListState
- rule.setContent {
- LazyColumnFor(
- items = items,
- state = rememberLazyListState().also { state = it },
- modifier = Modifier.height(itemSizeMinusOne).testTag(LazyColumnForTag)
- ) {
- Spacer(
- if (it == 0) {
- Modifier.width(30.dp).height(itemSizeMinusOne)
- } else {
- Modifier.width(20.dp).height(itemSize)
- }
- )
- }
- }
-
- state.scrollBy(itemSize)
-
- rule.onNodeWithTag(LazyColumnForTag)
- .assertWidthIsEqualTo(20.dp)
- }
-
- @Test
- fun itemStillVisibleAfterOverscrollIsAffectingCrossAxisSize() {
- val items = (0..2).toList()
- val itemSize = with(rule.density) { 30.toDp() }
- lateinit var state: LazyListState
- rule.setContent {
- LazyColumnFor(
- items = items,
- state = rememberLazyListState().also { state = it },
- modifier = Modifier.height(itemSize * 1.75f).testTag(LazyColumnForTag)
- ) {
- Spacer(
- if (it == 0) {
- Modifier.width(30.dp).height(itemSize / 2)
- } else if (it == 1) {
- Modifier.width(20.dp).height(itemSize / 2)
- } else {
- Modifier.width(20.dp).height(itemSize)
- }
- )
- }
- }
-
- state.scrollBy(itemSize)
-
- rule.onNodeWithTag(LazyColumnForTag)
- .assertWidthIsEqualTo(30.dp)
- }
-
- private fun SemanticsNodeInteraction.assertTopPositionIsAlmost(expected: Dp) {
- getUnclippedBoundsInRoot().top.assertIsEqualTo(expected, tolerance = 1.dp)
- }
-
- private fun LazyListState.scrollBy(offset: Dp) {
- runBlocking {
- smoothScrollBy(with(rule.density) { offset.toIntPx().toFloat() }, snap())
- }
- }
-}
-
-data class NotStable(val count: Int)
-
-internal fun IntegerSubject.isWithin1PixelFrom(expected: Int) {
- isEqualTo(expected, 1)
-}
-
-internal fun IntegerSubject.isEqualTo(expected: Int, tolerance: Int) {
- isIn(Range.closed(expected - tolerance, expected + tolerance))
-}
-
-internal fun SemanticsNodeInteraction.scrollBy(x: Dp = 0.dp, y: Dp = 0.dp, density: Density) =
- performGesture {
- with(density) {
- val touchSlop = TouchSlop.toIntPx()
- val xPx = x.toIntPx()
- val yPx = y.toIntPx()
- val offsetX = if (xPx > 0) xPx + touchSlop else if (xPx < 0) xPx - touchSlop else 0
- val offsetY = if (yPx > 0) yPx + touchSlop else if (yPx < 0) yPx - touchSlop else 0
- swipeWithVelocity(
- start = center,
- end = Offset(center.x - offsetX, center.y - offsetY),
- endVelocity = 0f
- )
- }
- }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnTest.kt
index 7e629eb3..d0c6400 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnTest.kt
@@ -16,101 +16,77 @@
package androidx.compose.foundation.lazy
+import androidx.compose.animation.core.ExponentialDecay
+import androidx.compose.animation.core.ManualAnimationClock
+import androidx.compose.animation.core.snap
+import androidx.compose.foundation.animation.FlingConfig
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.preferredHeight
import androidx.compose.foundation.layout.preferredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.onCommit
+import androidx.compose.runtime.onDispose
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.gesture.TouchSlop
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertCountEquals
+import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsEqualTo
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.center
+import androidx.compose.ui.test.click
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onChildren
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performGesture
+import androidx.compose.ui.test.swipeUp
+import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
+import androidx.test.filters.LargeTest
+import com.google.common.collect.Range
+import com.google.common.truth.IntegerSubject
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import java.util.concurrent.CountDownLatch
-@MediumTest
+@LargeTest
@RunWith(AndroidJUnit4::class)
class LazyColumnTest {
- private val LazyColumnTag = "LazyColumnTag"
+ private val LazyListTag = "LazyListTag"
@get:Rule
val rule = createComposeRule()
@Test
- fun lazyColumnShowsItem() {
- val itemTestTag = "itemTestTag"
-
- rule.setContent {
- LazyColumn {
- item {
- Spacer(
- Modifier.preferredHeight(10.dp).fillParentMaxWidth().testTag(itemTestTag)
- )
- }
- }
- }
-
- rule.onNodeWithTag(itemTestTag)
- .assertIsDisplayed()
- }
-
- @Test
- fun lazyColumnShowsItems() {
- val items = (1..4).map { it.toString() }
-
- rule.setContent {
- LazyColumn(Modifier.preferredHeight(200.dp)) {
- items(items) {
- Spacer(Modifier.preferredHeight(101.dp).fillParentMaxWidth().testTag(it))
- }
- }
- }
-
- rule.onNodeWithTag("1")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("2")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("3")
- .assertDoesNotExist()
-
- rule.onNodeWithTag("4")
- .assertDoesNotExist()
- }
-
- @Test
- fun lazyColumnShowsIndexedItems() {
- val items = (1..4).map { it.toString() }
-
- rule.setContent {
- LazyColumn(Modifier.preferredHeight(200.dp)) {
- itemsIndexed(items) { index, item ->
- Spacer(
- Modifier.preferredHeight(101.dp).fillParentMaxWidth()
- .testTag("$index-$item")
- )
- }
- }
- }
-
- rule.onNodeWithTag("0-1")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("1-2")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("2-3")
- .assertDoesNotExist()
-
- rule.onNodeWithTag("3-4")
- .assertDoesNotExist()
- }
-
- @Test
fun lazyColumnShowsCombinedItems() {
val itemTestTag = "itemTestTag"
val items = listOf(1, 2).map { it.toString() }
@@ -155,59 +131,6 @@
}
@Test
- fun lazyColumnShowsItemsOnScroll() {
- val items = (1..4).map { it.toString() }
-
- rule.setContent {
- LazyColumn(Modifier.preferredHeight(200.dp).testTag(LazyColumnTag)) {
- items(items) {
- Spacer(Modifier.preferredHeight(101.dp).fillParentMaxWidth().testTag(it))
- }
- }
- }
-
- rule.onNodeWithTag(LazyColumnTag)
- .scrollBy(y = 50.dp, density = rule.density)
-
- rule.onNodeWithTag("1")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("2")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("3")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("4")
- .assertDoesNotExist()
- }
-
- @Test
- fun lazyColumnScrollHidesItem() {
- val items = (1..4).map { it.toString() }
-
- rule.setContent {
- LazyColumn(Modifier.preferredHeight(200.dp).testTag(LazyColumnTag)) {
- items(items) {
- Spacer(Modifier.preferredHeight(101.dp).fillParentMaxWidth().testTag(it))
- }
- }
- }
-
- rule.onNodeWithTag(LazyColumnTag)
- .scrollBy(y = 103.dp, density = rule.density)
-
- rule.onNodeWithTag("1")
- .assertDoesNotExist()
-
- rule.onNodeWithTag("2")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("3")
- .assertIsDisplayed()
- }
-
- @Test
fun lazyColumnAllowEmptyListItems() {
val itemTag = "itemTag"
@@ -253,4 +176,1050 @@
rule.onNodeWithTag("3")
.assertDoesNotExist()
}
-}
\ No newline at end of file
+
+ @Test
+ fun compositionsAreDisposed_whenNodesAreScrolledOff() {
+ var composed: Boolean
+ var disposed = false
+ // Ten 31dp spacers in a 300dp list
+ val latch = CountDownLatch(10)
+ // Make it long enough that it's _definitely_ taller than the screen
+ val data = (1..50).toList()
+
+ rule.setContent {
+ // Fixed height to eliminate device size as a factor
+ Box(Modifier.testTag(LazyListTag).preferredHeight(300.dp)) {
+ LazyColumn(Modifier.fillMaxSize()) {
+ items(data) {
+ onCommit {
+ composed = true
+ // Signal when everything is done composing
+ latch.countDown()
+ onDispose {
+ disposed = true
+ }
+ }
+
+ // There will be 10 of these in the 300dp box
+ Spacer(Modifier.preferredHeight(31.dp))
+ }
+ }
+ }
+ }
+
+ latch.await()
+ composed = false
+
+ assertWithMessage("Compositions were disposed before we did any scrolling")
+ .that(disposed).isFalse()
+
+ // Mostly a validity check, this is not part of the behavior under test
+ assertWithMessage("Additional composition occurred for no apparent reason")
+ .that(composed).isFalse()
+
+ rule.onNodeWithTag(LazyListTag)
+ .performGesture { swipeUp() }
+
+ rule.waitForIdle()
+
+ assertWithMessage("No additional items were composed after scroll, scroll didn't work")
+ .that(composed).isTrue()
+
+ // We may need to modify this test once we prefetch/cache items outside the viewport
+ assertWithMessage(
+ "No compositions were disposed after scrolling, compositions were leaked"
+ ).that(disposed).isTrue()
+ }
+
+ @Test
+ fun compositionsAreDisposed_whenDataIsChanged() {
+ var composed = 0
+ var disposals = 0
+ val data1 = (1..3).toList()
+ val data2 = (4..5).toList() // smaller, to ensure removal is handled properly
+
+ var part2 by mutableStateOf(false)
+
+ rule.setContent {
+ LazyColumn(Modifier.testTag(LazyListTag).fillMaxSize()) {
+ items(if (!part2) data1 else data2) {
+ onCommit {
+ composed++
+ onDispose {
+ disposals++
+ }
+ }
+
+ Spacer(Modifier.height(50.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertWithMessage("Not all items were composed")
+ .that(composed).isEqualTo(data1.size)
+ composed = 0
+
+ part2 = true
+ }
+
+ rule.runOnIdle {
+ assertWithMessage(
+ "No additional items were composed after data change, something didn't work"
+ ).that(composed).isEqualTo(data2.size)
+
+ // We may need to modify this test once we prefetch/cache items outside the viewport
+ assertWithMessage(
+ "Not enough compositions were disposed after scrolling, compositions were leaked"
+ ).that(disposals).isEqualTo(data1.size)
+ }
+ }
+
+ @Test
+ fun compositionsAreDisposed_whenAdapterListIsDisposed() {
+ var emitAdapterList by mutableStateOf(true)
+ var disposeCalledOnFirstItem = false
+ var disposeCalledOnSecondItem = false
+
+ rule.setContent {
+ if (emitAdapterList) {
+ LazyColumn(Modifier.fillMaxSize()) {
+ items(listOf(0, 1)) {
+ Box(Modifier.size(100.dp))
+ onDispose {
+ if (it == 1) {
+ disposeCalledOnFirstItem = true
+ } else {
+ disposeCalledOnSecondItem = true
+ }
+ }
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertWithMessage("First item is not immediately disposed")
+ .that(disposeCalledOnFirstItem).isFalse()
+ assertWithMessage("Second item is not immediately disposed")
+ .that(disposeCalledOnFirstItem).isFalse()
+ emitAdapterList = false
+ }
+
+ rule.runOnIdle {
+ assertWithMessage("First item is correctly disposed")
+ .that(disposeCalledOnFirstItem).isTrue()
+ assertWithMessage("Second item is correctly disposed")
+ .that(disposeCalledOnSecondItem).isTrue()
+ }
+ }
+
+ @Test
+ fun removeItemsTest() {
+ val startingNumItems = 3
+ var numItems = startingNumItems
+ var numItemsModel by mutableStateOf(numItems)
+ val tag = "List"
+ rule.setContent {
+ LazyColumn(Modifier.testTag(tag)) {
+ items((1..numItemsModel).toList()) {
+ BasicText("$it")
+ }
+ }
+ }
+
+ while (numItems >= 0) {
+ // Confirm the number of children to ensure there are no extra items
+ rule.onNodeWithTag(tag)
+ .onChildren()
+ .assertCountEquals(numItems)
+
+ // Confirm the children's content
+ for (i in 1..3) {
+ rule.onNodeWithText("$i").apply {
+ if (i <= numItems) {
+ assertExists()
+ } else {
+ assertDoesNotExist()
+ }
+ }
+ }
+ numItems--
+ if (numItems >= 0) {
+ // Don't set the model to -1
+ rule.runOnIdle { numItemsModel = numItems }
+ }
+ }
+ }
+
+ @Test
+ fun changingDataTest() {
+ val dataLists = listOf(
+ (1..3).toList(),
+ (4..8).toList(),
+ (3..4).toList()
+ )
+ var dataModel by mutableStateOf(dataLists[0])
+ val tag = "List"
+ rule.setContent {
+ LazyColumn(Modifier.testTag(tag)) {
+ items(dataModel) {
+ BasicText("$it")
+ }
+ }
+ }
+
+ for (data in dataLists) {
+ rule.runOnIdle { dataModel = data }
+
+ // Confirm the number of children to ensure there are no extra items
+ val numItems = data.size
+ rule.onNodeWithTag(tag)
+ .onChildren()
+ .assertCountEquals(numItems)
+
+ // Confirm the children's content
+ for (item in data) {
+ rule.onNodeWithText("$item").assertExists()
+ }
+ }
+ }
+
+ @Test
+ fun whenItemsAreInitiallyCreatedWith0SizeWeCanScrollWhenTheyExpanded() {
+ val thirdTag = "third"
+ val items = (1..3).toList()
+ var thirdHasSize by mutableStateOf(false)
+
+ rule.setContent {
+ LazyColumn(
+ Modifier.fillMaxWidth()
+ .preferredHeight(100.dp)
+ .testTag(LazyListTag)
+ ) {
+ items(items) {
+ if (it == 3) {
+ Spacer(
+ Modifier.testTag(thirdTag)
+ .fillParentMaxWidth()
+ .preferredHeight(if (thirdHasSize) 60.dp else 0.dp)
+ )
+ } else {
+ Spacer(Modifier.fillParentMaxWidth().preferredHeight(60.dp))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(y = 21.dp, density = rule.density)
+
+ rule.onNodeWithTag(thirdTag)
+ .assertExists()
+ .assertIsNotDisplayed()
+
+ rule.runOnIdle {
+ thirdHasSize = true
+ }
+
+ rule.waitForIdle()
+
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(y = 10.dp, density = rule.density)
+
+ rule.onNodeWithTag(thirdTag)
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun lazyColumnWrapsContent() = with(rule.density) {
+ val itemInsideLazyColumn = "itemInsideLazyColumn"
+ val itemOutsideLazyColumn = "itemOutsideLazyColumn"
+ var sameSizeItems by mutableStateOf(true)
+
+ rule.setContent {
+ Row {
+ LazyColumn(Modifier.testTag(LazyListTag)) {
+ items(listOf(1, 2)) {
+ if (it == 1) {
+ Spacer(Modifier.preferredSize(50.dp).testTag(itemInsideLazyColumn))
+ } else {
+ Spacer(Modifier.preferredSize(if (sameSizeItems) 50.dp else 70.dp))
+ }
+ }
+ }
+ Spacer(Modifier.preferredSize(50.dp).testTag(itemOutsideLazyColumn))
+ }
+ }
+
+ rule.onNodeWithTag(itemInsideLazyColumn)
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag(itemOutsideLazyColumn)
+ .assertIsDisplayed()
+
+ var lazyColumnBounds = rule.onNodeWithTag(LazyListTag)
+ .getUnclippedBoundsInRoot()
+
+ assertThat(lazyColumnBounds.left.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
+ assertThat(lazyColumnBounds.right.toIntPx()).isWithin1PixelFrom(50.dp.toIntPx())
+ assertThat(lazyColumnBounds.top.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
+ assertThat(lazyColumnBounds.bottom.toIntPx()).isWithin1PixelFrom(100.dp.toIntPx())
+
+ rule.runOnIdle {
+ sameSizeItems = false
+ }
+
+ rule.waitForIdle()
+
+ rule.onNodeWithTag(itemInsideLazyColumn)
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag(itemOutsideLazyColumn)
+ .assertIsDisplayed()
+
+ lazyColumnBounds = rule.onNodeWithTag(LazyListTag)
+ .getUnclippedBoundsInRoot()
+
+ assertThat(lazyColumnBounds.left.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
+ assertThat(lazyColumnBounds.right.toIntPx()).isWithin1PixelFrom(70.dp.toIntPx())
+ assertThat(lazyColumnBounds.top.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
+ assertThat(lazyColumnBounds.bottom.toIntPx()).isWithin1PixelFrom(120.dp.toIntPx())
+ }
+
+ private val firstItemTag = "firstItemTag"
+ private val secondItemTag = "secondItemTag"
+
+ private fun prepareLazyColumnsItemsAlignment(horizontalGravity: Alignment.Horizontal) {
+ rule.setContent {
+ LazyColumn(
+ Modifier.testTag(LazyListTag).width(100.dp),
+ horizontalAlignment = horizontalGravity
+ ) {
+ items(listOf(1, 2)) {
+ if (it == 1) {
+ Spacer(Modifier.preferredSize(50.dp).testTag(firstItemTag))
+ } else {
+ Spacer(Modifier.preferredSize(70.dp).testTag(secondItemTag))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag(secondItemTag)
+ .assertIsDisplayed()
+
+ val lazyColumnBounds = rule.onNodeWithTag(LazyListTag)
+ .getUnclippedBoundsInRoot()
+
+ with(rule.density) {
+ // Verify the width of the column
+ assertThat(lazyColumnBounds.left.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
+ assertThat(lazyColumnBounds.right.toIntPx()).isWithin1PixelFrom(100.dp.toIntPx())
+ }
+ }
+
+ @Test
+ fun lazyColumnAlignmentCenterHorizontally() {
+ prepareLazyColumnsItemsAlignment(Alignment.CenterHorizontally)
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertPositionInRootIsEqualTo(25.dp, 0.dp)
+
+ rule.onNodeWithTag(secondItemTag)
+ .assertPositionInRootIsEqualTo(15.dp, 50.dp)
+ }
+
+ @Test
+ fun lazyColumnAlignmentStart() {
+ prepareLazyColumnsItemsAlignment(Alignment.Start)
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+
+ rule.onNodeWithTag(secondItemTag)
+ .assertPositionInRootIsEqualTo(0.dp, 50.dp)
+ }
+
+ @Test
+ fun lazyColumnAlignmentEnd() {
+ prepareLazyColumnsItemsAlignment(Alignment.End)
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertPositionInRootIsEqualTo(50.dp, 0.dp)
+
+ rule.onNodeWithTag(secondItemTag)
+ .assertPositionInRootIsEqualTo(30.dp, 50.dp)
+ }
+
+ @Test
+ fun itemFillingParentWidth() {
+ rule.setContent {
+ LazyColumn(Modifier.size(width = 100.dp, height = 150.dp)) {
+ items(listOf(0)) {
+ Spacer(Modifier.fillParentMaxWidth().height(50.dp).testTag(firstItemTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(100.dp)
+ .assertHeightIsEqualTo(50.dp)
+ }
+
+ @Test
+ fun itemFillingParentHeight() {
+ rule.setContent {
+ LazyColumn(Modifier.size(width = 100.dp, height = 150.dp)) {
+ items(listOf(0)) {
+ Spacer(Modifier.width(50.dp).fillParentMaxHeight().testTag(firstItemTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(150.dp)
+ }
+
+ @Test
+ fun itemFillingParentSize() {
+ rule.setContent {
+ LazyColumn(Modifier.size(width = 100.dp, height = 150.dp)) {
+ items(listOf(0)) {
+ Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(100.dp)
+ .assertHeightIsEqualTo(150.dp)
+ }
+
+ @Test
+ fun itemFillingParentWidthFraction() {
+ rule.setContent {
+ LazyColumn(Modifier.size(width = 100.dp, height = 150.dp)) {
+ items(listOf(0)) {
+ Spacer(Modifier.fillParentMaxWidth(0.6f).height(50.dp).testTag(firstItemTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(60.dp)
+ .assertHeightIsEqualTo(50.dp)
+ }
+
+ @Test
+ fun itemFillingParentHeightFraction() {
+ rule.setContent {
+ LazyColumn(Modifier.size(width = 100.dp, height = 150.dp)) {
+ items(listOf(0)) {
+ Spacer(Modifier.width(50.dp).fillParentMaxHeight(0.2f).testTag(firstItemTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(30.dp)
+ }
+
+ @Test
+ fun itemFillingParentSizeFraction() {
+ rule.setContent {
+ LazyColumn(Modifier.size(width = 100.dp, height = 150.dp)) {
+ items(listOf(0)) {
+ Spacer(Modifier.fillParentMaxSize(0.1f).testTag(firstItemTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(10.dp)
+ .assertHeightIsEqualTo(15.dp)
+ }
+
+ @Test
+ fun itemFillingParentSizeParentResized() {
+ var parentSize by mutableStateOf(100.dp)
+ rule.setContent {
+ LazyColumn(Modifier.size(parentSize)) {
+ items(listOf(0)) {
+ Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ parentSize = 150.dp
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(150.dp)
+ .assertHeightIsEqualTo(150.dp)
+ }
+
+ @Test
+ fun whenNotAnymoreAvailableItemWasDisplayed() {
+ var items by mutableStateOf((1..30).toList())
+ rule.setContent {
+ LazyColumn(Modifier.size(100.dp).testTag(LazyListTag)) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ // after scroll we will display items 16-20
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(y = 300.dp, density = rule.density)
+
+ rule.runOnIdle {
+ items = (1..10).toList()
+ }
+
+ // there is no item 16 anymore so we will just display the last items 6-10
+ rule.onNodeWithTag("6")
+ .assertTopPositionIsAlmost(0.dp)
+ }
+
+ @Test
+ fun whenFewDisplayedItemsWereRemoved() {
+ var items by mutableStateOf((1..10).toList())
+ rule.setContent {
+ LazyColumn(Modifier.size(100.dp).testTag(LazyListTag)) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ // after scroll we will display items 6-10
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(y = 100.dp, density = rule.density)
+
+ rule.runOnIdle {
+ items = (1..8).toList()
+ }
+
+ // there are no more items 9 and 10, so we have to scroll back
+ rule.onNodeWithTag("4")
+ .assertTopPositionIsAlmost(0.dp)
+ }
+
+ @Test
+ fun whenItemsBecameEmpty() {
+ var items by mutableStateOf((1..10).toList())
+ rule.setContent {
+ LazyColumn(Modifier.sizeIn(maxHeight = 100.dp).testTag(LazyListTag)) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ // after scroll we will display items 2-6
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(y = 20.dp, density = rule.density)
+
+ rule.runOnIdle {
+ items = emptyList()
+ }
+
+ // there are no more items so the LazyColumn is zero sized
+ rule.onNodeWithTag(LazyListTag)
+ .assertWidthIsEqualTo(0.dp)
+ .assertHeightIsEqualTo(0.dp)
+
+ // and has no children
+ rule.onNodeWithTag("1")
+ .assertDoesNotExist()
+ rule.onNodeWithTag("2")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun scrollBackAndForth() {
+ val items by mutableStateOf((1..20).toList())
+ rule.setContent {
+ LazyColumn(Modifier.size(100.dp).testTag(LazyListTag)) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ // after scroll we will display items 6-10
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(y = 100.dp, density = rule.density)
+
+ // and scroll back
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(y = (-100).dp, density = rule.density)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionIsAlmost(0.dp)
+ }
+
+ @Test
+ fun tryToScrollBackwardWhenAlreadyOnTop() {
+ val items by mutableStateOf((1..20).toList())
+ rule.setContent {
+ LazyColumn(Modifier.size(100.dp).testTag(LazyListTag)) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ // we already displaying the first item, so this should do nothing
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(y = (-50).dp, density = rule.density)
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionIsAlmost(0.dp)
+ rule.onNodeWithTag("5")
+ .assertTopPositionIsAlmost(80.dp)
+ }
+
+ @Test
+ fun contentOfNotStableItemsIsNotRecomposedDuringScroll() {
+ val items = listOf(NotStable(1), NotStable(2))
+ var firstItemRecomposed = 0
+ var secondItemRecomposed = 0
+ rule.setContent {
+ LazyColumn(Modifier.size(100.dp).testTag(LazyListTag)) {
+ items(items) {
+ if (it.count == 1) {
+ firstItemRecomposed++
+ } else {
+ secondItemRecomposed++
+ }
+ Spacer(Modifier.size(75.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(firstItemRecomposed).isEqualTo(1)
+ assertThat(secondItemRecomposed).isEqualTo(1)
+ }
+
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(y = (50).dp, density = rule.density)
+
+ rule.runOnIdle {
+ assertThat(firstItemRecomposed).isEqualTo(1)
+ assertThat(secondItemRecomposed).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun onlyOneMeasurePassForScrollEvent() {
+ val items by mutableStateOf((1..20).toList())
+ lateinit var state: LazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ LazyColumn(
+ Modifier.size(100.dp).testTag(LazyListTag),
+ state = state
+ ) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ val initialMeasurePasses = state.numMeasurePasses
+
+ rule.runOnIdle {
+ with(rule.density) {
+ state.onScroll(-110.dp.toPx())
+ }
+ }
+
+ rule.waitForIdle()
+
+ assertThat(state.numMeasurePasses).isEqualTo(initialMeasurePasses + 1)
+ }
+
+ @Test
+ fun stateUpdatedAfterScroll() {
+ val items by mutableStateOf((1..20).toList())
+ lateinit var state: LazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ LazyColumn(
+ Modifier.size(100.dp).testTag(LazyListTag),
+ state = state
+ ) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ }
+
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(y = 30.dp, density = rule.density)
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+
+ with(rule.density) {
+ // TODO(b/169232491): test scrolling doesn't appear to be scrolling exactly the right
+ // number of pixels
+ val expectedOffset = 10.dp.toIntPx()
+ val tolerance = 2.dp.toIntPx()
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset, tolerance)
+ }
+ }
+ }
+
+ @Test
+ fun isAnimationRunningUpdate() {
+ val items by mutableStateOf((1..20).toList())
+ val clock = ManualAnimationClock(0L)
+ val state = LazyListState(
+ flingConfig = FlingConfig(ExponentialDecay()),
+ animationClock = clock
+ )
+ rule.setContent {
+ LazyColumn(
+ Modifier.size(100.dp).testTag(LazyListTag),
+ state = state
+ ) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ assertThat(state.isAnimationRunning).isEqualTo(false)
+ }
+
+ rule.onNodeWithTag(LazyListTag)
+ .performGesture { swipeUp() }
+
+ rule.runOnIdle {
+ clock.clockTimeMillis += 100
+ assertThat(state.firstVisibleItemIndex).isNotEqualTo(0)
+ assertThat(state.isAnimationRunning).isEqualTo(true)
+ }
+
+ // TODO (jelle): this should be down, and not click to be 100% fair
+ rule.onNodeWithTag(LazyListTag)
+ .performGesture { click() }
+
+ rule.runOnIdle {
+ assertThat(state.isAnimationRunning).isEqualTo(false)
+ }
+ }
+
+ @Test
+ fun stateUpdatedAfterScrollWithinTheSameItem() {
+ val items by mutableStateOf((1..20).toList())
+ lateinit var state: LazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ LazyColumn(
+ Modifier.size(100.dp).testTag(LazyListTag),
+ state = state
+ ) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(y = 10.dp, density = rule.density)
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ with(rule.density) {
+ val expectedOffset = 10.dp.toIntPx()
+ val tolerance = 2.dp.toIntPx()
+ assertThat(state.firstVisibleItemScrollOffset)
+ .isEqualTo(expectedOffset, tolerance)
+ }
+ }
+ }
+
+ @Test
+ fun initialScrollIsApplied() {
+ val items by mutableStateOf((0..20).toList())
+ lateinit var state: LazyListState
+ val expectedOffset = with(rule.density) { 10.dp.toIntPx() }
+ rule.setContent {
+ state = rememberLazyListState(2, expectedOffset)
+ LazyColumn(
+ Modifier.size(100.dp).testTag(LazyListTag),
+ state = state
+ ) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(2)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset)
+ }
+
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo((-10).dp)
+ }
+
+ @Test
+ fun stateIsRestored() {
+ val restorationTester = StateRestorationTester(rule)
+ val items by mutableStateOf((1..20).toList())
+ var state: LazyListState? = null
+ restorationTester.setContent {
+ state = rememberLazyListState()
+ LazyColumn(
+ Modifier.size(100.dp).testTag(LazyListTag),
+ state = state!!
+ ) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(y = 30.dp, density = rule.density)
+
+ val (index, scrollOffset) = rule.runOnIdle {
+ state!!.firstVisibleItemIndex to state!!.firstVisibleItemScrollOffset
+ }
+
+ state = null
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ rule.runOnIdle {
+ assertThat(state!!.firstVisibleItemIndex).isEqualTo(index)
+ assertThat(state!!.firstVisibleItemScrollOffset).isEqualTo(scrollOffset)
+ }
+ }
+
+ @Test
+ fun scroll_makeListSmaller_scroll() {
+ var items by mutableStateOf((1..100).toList())
+ rule.setContent {
+ LazyColumn(Modifier.size(100.dp).testTag(LazyListTag)) {
+ items(items) {
+ Spacer(Modifier.size(10.dp).testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(y = 300.dp, density = rule.density)
+
+ rule.runOnIdle {
+ items = (1..11).toList()
+ }
+
+ // try to scroll after the data set has been updated. this was causing a crash previously
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(y = (-10).dp, density = rule.density)
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun snapToItemIndex() {
+ val items by mutableStateOf((1..20).toList())
+ lateinit var state: LazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ LazyColumn(
+ Modifier.size(100.dp).testTag(LazyListTag),
+ state = state
+ ) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.snapToItemIndex(3, 10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(3)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+ }
+ }
+
+ @Test
+ fun itemsAreNotRedrawnDuringScroll() {
+ val items = (0..20).toList()
+ val redrawCount = Array(6) { 0 }
+ rule.setContent {
+ LazyColumn(Modifier.size(100.dp).testTag(LazyListTag)) {
+ items(items) {
+ Spacer(
+ Modifier.size(20.dp)
+ .drawBehind { redrawCount[it]++ }
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(y = 10.dp, density = rule.density)
+
+ rule.runOnIdle {
+ redrawCount.forEachIndexed { index, i ->
+ assertWithMessage("Item with index $index was redrawn $i times")
+ .that(i).isEqualTo(1)
+ }
+ }
+ }
+
+ @Test
+ fun itemInvalidationIsNotCausingAnotherItemToRedraw() {
+ val items = (0..1).toList()
+ val redrawCount = Array(2) { 0 }
+ var stateUsedInDrawScope by mutableStateOf(false)
+ rule.setContent {
+ LazyColumn(Modifier.size(100.dp).testTag(LazyListTag)) {
+ items(items) {
+ Spacer(
+ Modifier.size(50.dp)
+ .drawBehind {
+ redrawCount[it]++
+ if (it == 1) {
+ stateUsedInDrawScope.hashCode()
+ }
+ }
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ stateUsedInDrawScope = true
+ }
+
+ rule.runOnIdle {
+ assertWithMessage("First items is not expected to be redrawn")
+ .that(redrawCount[0]).isEqualTo(1)
+ assertWithMessage("Second items is expected to be redrawn")
+ .that(redrawCount[1]).isEqualTo(2)
+ }
+ }
+
+ @Test
+ fun notVisibleAnymoreItemNotAffectingCrossAxisSize() {
+ val items = (0..1).toList()
+ val itemSize = with(rule.density) { 30.toDp() }
+ val itemSizeMinusOne = with(rule.density) { 29.toDp() }
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyColumn(
+ Modifier.height(itemSizeMinusOne).testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it }
+ ) {
+ items(items) {
+ Spacer(
+ if (it == 0) {
+ Modifier.width(30.dp).height(itemSizeMinusOne)
+ } else {
+ Modifier.width(20.dp).height(itemSize)
+ }
+ )
+ }
+ }
+ }
+
+ state.scrollBy(itemSize)
+
+ rule.onNodeWithTag(LazyListTag)
+ .assertWidthIsEqualTo(20.dp)
+ }
+
+ @Test
+ fun itemStillVisibleAfterOverscrollIsAffectingCrossAxisSize() {
+ val items = (0..2).toList()
+ val itemSize = with(rule.density) { 30.toDp() }
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyColumn(
+ Modifier.height(itemSize * 1.75f).testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it }
+ ) {
+ items(items) {
+ Spacer(
+ if (it == 0) {
+ Modifier.width(30.dp).height(itemSize / 2)
+ } else if (it == 1) {
+ Modifier.width(20.dp).height(itemSize / 2)
+ } else {
+ Modifier.width(20.dp).height(itemSize)
+ }
+ )
+ }
+ }
+ }
+
+ state.scrollBy(itemSize)
+
+ rule.onNodeWithTag(LazyListTag)
+ .assertWidthIsEqualTo(30.dp)
+ }
+
+ private fun SemanticsNodeInteraction.assertTopPositionIsAlmost(expected: Dp) {
+ getUnclippedBoundsInRoot().top.assertIsEqualTo(expected, tolerance = 1.dp)
+ }
+
+ private fun LazyListState.scrollBy(offset: Dp) {
+ runBlocking {
+ smoothScrollBy(with(rule.density) { offset.toIntPx().toFloat() }, snap())
+ }
+ }
+}
+
+data class NotStable(val count: Int)
+
+internal fun IntegerSubject.isWithin1PixelFrom(expected: Int) {
+ isEqualTo(expected, 1)
+}
+
+internal fun IntegerSubject.isEqualTo(expected: Int, tolerance: Int) {
+ isIn(Range.closed(expected - tolerance, expected + tolerance))
+}
+
+internal fun SemanticsNodeInteraction.scrollBy(x: Dp = 0.dp, y: Dp = 0.dp, density: Density) =
+ performGesture {
+ with(density) {
+ val touchSlop = TouchSlop.toIntPx()
+ val xPx = x.toIntPx()
+ val yPx = y.toIntPx()
+ val offsetX = if (xPx > 0) xPx + touchSlop else if (xPx < 0) xPx - touchSlop else 0
+ val offsetY = if (yPx > 0) yPx + touchSlop else if (yPx < 0) yPx - touchSlop else 0
+ swipeWithVelocity(
+ start = center,
+ end = Offset(center.x - offsetX, center.y - offsetY),
+ endVelocity = 0f
+ )
+ }
+ }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyForIndexedTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyForIndexedTest.kt
deleted file mode 100644
index 37c72b8..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyForIndexedTest.kt
+++ /dev/null
@@ -1,71 +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.compose.foundation.lazy
-
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.preferredHeight
-import androidx.compose.foundation.layout.preferredWidth
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.text.BasicText
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.unit.dp
-import org.junit.Rule
-import org.junit.Test
-
-class LazyForIndexedTest {
-
- @get:Rule
- val rule = createComposeRule()
-
- @Test
- fun columnWithIndexesComposedWithCorrectIndexAndItem() {
- val items = (0..1).map { it.toString() }
-
- rule.setContent {
- LazyColumnForIndexed(items, Modifier.preferredHeight(200.dp)) { index, item ->
- BasicText("${index}x$item", Modifier.fillParentMaxWidth().height(100.dp))
- }
- }
-
- rule.onNodeWithText("0x0")
- .assertTopPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithText("1x1")
- .assertTopPositionInRootIsEqualTo(100.dp)
- }
-
- @Test
- fun rowWithIndexesComposedWithCorrectIndexAndItem() {
- val items = (0..1).map { it.toString() }
-
- rule.setContent {
- LazyRowForIndexed(items, Modifier.preferredWidth(200.dp)) { index, item ->
- BasicText("${index}x$item", Modifier.fillParentMaxHeight().width(100.dp))
- }
- }
-
- rule.onNodeWithText("0x0")
- .assertLeftPositionInRootIsEqualTo(0.dp)
-
- rule.onNodeWithText("1x1")
- .assertLeftPositionInRootIsEqualTo(100.dp)
- }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyItemStateRestoration.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyItemStateRestoration.kt
new file mode 100644
index 0000000..5dc3b6f
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyItemStateRestoration.kt
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.onDispose
+import androidx.compose.runtime.savedinstancestate.rememberSavedInstanceState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.StateRestorationTester
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+
+class LazyItemStateRestoration {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun visibleItemsStateRestored() {
+ val restorationTester = StateRestorationTester(rule)
+ var counter0 = 1
+ var counter1 = 10
+ var counter2 = 100
+ var realState = arrayOf(0, 0, 0)
+ restorationTester.setContent {
+ LazyColumn {
+ item {
+ realState[0] = rememberSavedInstanceState { counter0++ }
+ Box(Modifier.size(1.dp))
+ }
+ items((1..2).toList()) {
+ if (it == 1) {
+ realState[1] = rememberSavedInstanceState { counter1++ }
+ } else {
+ realState[2] = rememberSavedInstanceState { counter2++ }
+ }
+ Box(Modifier.size(1.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState[0]).isEqualTo(1)
+ assertThat(realState[1]).isEqualTo(10)
+ assertThat(realState[2]).isEqualTo(100)
+ realState = arrayOf(0, 0, 0)
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ rule.runOnIdle {
+ assertThat(realState[0]).isEqualTo(1)
+ assertThat(realState[1]).isEqualTo(10)
+ assertThat(realState[2]).isEqualTo(100)
+ }
+ }
+
+ @Test
+ fun itemsStateRestoredWhenWeScrolledBackToIt() {
+ val restorationTester = StateRestorationTester(rule)
+ var counter0 = 1
+ lateinit var state: LazyListState
+ var itemDisposed = false
+ var realState = 0
+ restorationTester.setContent {
+ LazyColumn(
+ Modifier.size(20.dp),
+ state = rememberLazyListState().also { state = it }
+ ) {
+ items((0..1).toList()) {
+ if (it == 0) {
+ realState = rememberSavedInstanceState { counter0++ }
+ onDispose {
+ itemDisposed = true
+ }
+ }
+ Box(Modifier.size(30.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState).isEqualTo(1)
+ runBlocking {
+ state.snapToItemIndex(1, 5)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(itemDisposed).isEqualTo(true)
+ realState = 0
+ runBlocking {
+ state.snapToItemIndex(0, 0)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun itemsStateRestoredWhenWeScrolledRestoredAndScrolledBackTo() {
+ val restorationTester = StateRestorationTester(rule)
+ var counter0 = 1
+ var counter1 = 10
+ lateinit var state: LazyListState
+ var realState = arrayOf(0, 0)
+ restorationTester.setContent {
+ LazyColumn(
+ Modifier.size(20.dp),
+ state = rememberLazyListState().also { state = it }
+ ) {
+ items((0..1).toList()) {
+ if (it == 0) {
+ realState[0] = rememberSavedInstanceState { counter0++ }
+ } else {
+ realState[1] = rememberSavedInstanceState { counter1++ }
+ }
+ Box(Modifier.size(30.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState[0]).isEqualTo(1)
+ runBlocking {
+ state.snapToItemIndex(1, 5)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState[1]).isEqualTo(10)
+ realState = arrayOf(0, 0)
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ rule.runOnIdle {
+ assertThat(realState[1]).isEqualTo(10)
+ runBlocking {
+ state.snapToItemIndex(0, 0)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState[0]).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun nestedLazy_itemsStateRestoredWhenWeScrolledBackToIt() {
+ val restorationTester = StateRestorationTester(rule)
+ var counter0 = 1
+ lateinit var state: LazyListState
+ var itemDisposed = false
+ var realState = 0
+ restorationTester.setContent {
+ LazyColumn(
+ Modifier.size(20.dp),
+ state = rememberLazyListState().also { state = it }
+ ) {
+ items((0..1).toList()) {
+ if (it == 0) {
+ LazyRow {
+ item {
+ realState = rememberSavedInstanceState { counter0++ }
+ onDispose {
+ itemDisposed = true
+ }
+ Box(Modifier.size(30.dp))
+ }
+ }
+ } else {
+ Box(Modifier.size(30.dp))
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState).isEqualTo(1)
+ runBlocking {
+ state.snapToItemIndex(1, 5)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(itemDisposed).isEqualTo(true)
+ realState = 0
+ runBlocking {
+ state.snapToItemIndex(0, 0)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(realState).isEqualTo(1)
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfoTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfoTest.kt
new file mode 100644
index 0000000..c5d040e
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfoTest.kt
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+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.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazyListLayoutInfoTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private var itemSizePx: Int = 50
+ private var itemSizeDp: Dp = Dp.Infinity
+
+ @Before
+ fun before() {
+ with(rule.density) {
+ itemSizeDp = itemSizePx.toDp()
+ }
+ }
+
+ @Test
+ fun visibleItemsAreCorrect() {
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyColumn(
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(itemSizeDp * 3.5f)
+ ) {
+ items((0..5).toList()) {
+ Box(Modifier.size(itemSizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ state.layoutInfo.assertVisibleItems(count = 4)
+ }
+ }
+
+ @Test
+ fun visibleItemsAreCorrectAfterScroll() {
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyColumn(
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(itemSizeDp * 3.5f)
+ ) {
+ items((0..5).toList()) {
+ Box(Modifier.size(itemSizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.snapToItemIndex(1, 10)
+ }
+ state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1, startOffset = -10)
+ }
+ }
+
+ @Test
+ fun visibleItemsAreCorrectWithSpacing() {
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyColumn(
+ state = rememberLazyListState().also { state = it },
+ verticalArrangement = Arrangement.spacedBy(itemSizeDp),
+ modifier = Modifier.size(itemSizeDp * 3.5f)
+ ) {
+ items((0..5).toList()) {
+ Box(Modifier.size(itemSizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ state.layoutInfo.assertVisibleItems(count = 2, spacing = itemSizePx)
+ }
+ }
+
+ @Test
+ fun visibleItemsAreObservableWhenWeScroll() {
+ lateinit var state: LazyListState
+ var currentInfo: LazyListLayoutInfo? = null
+ @Composable
+ fun observingFun() {
+ currentInfo = state.layoutInfo
+ }
+ rule.setContent {
+ LazyColumn(
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(itemSizeDp * 3.5f)
+ ) {
+ items((0..5).toList()) {
+ Box(Modifier.size(itemSizeDp))
+ }
+ }
+ observingFun()
+ }
+
+ rule.runOnIdle {
+ // empty it here and scrolling should invoke observingFun again
+ currentInfo = null
+ runBlocking {
+ state.snapToItemIndex(1, 0)
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(currentInfo).isNotNull()
+ currentInfo!!.assertVisibleItems(count = 4, startIndex = 1)
+ }
+ }
+
+ @Test
+ fun visibleItemsAreObservableWhenResize() {
+ lateinit var state: LazyListState
+ var size by mutableStateOf(itemSizeDp * 2)
+ var currentInfo: LazyListLayoutInfo? = null
+ @Composable
+ fun observingFun() {
+ currentInfo = state.layoutInfo
+ }
+ rule.setContent {
+ LazyColumn(
+ state = rememberLazyListState().also { state = it }
+ ) {
+ item {
+ Box(Modifier.size(size))
+ }
+ }
+ observingFun()
+ }
+
+ rule.runOnIdle {
+ assertThat(currentInfo).isNotNull()
+ currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx * 2)
+ currentInfo = null
+ size = itemSizeDp
+ }
+
+ rule.runOnIdle {
+ assertThat(currentInfo).isNotNull()
+ currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx)
+ }
+ }
+
+ @Test
+ fun totalCountIsCorrect() {
+ var count by mutableStateOf(10)
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyColumn(
+ state = rememberLazyListState().also { state = it }
+ ) {
+ items((0 until count).toList()) {
+ Box(Modifier.size(10.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.totalItemsCount).isEqualTo(10)
+ count = 20
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.totalItemsCount).isEqualTo(20)
+ }
+ }
+
+ @Test
+ fun viewportOffsetsAreCorrect() {
+ val sizePx = 45
+ val sizeDp = with(rule.density) { sizePx.toDp() }
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyColumn(
+ Modifier.size(sizeDp),
+ state = rememberLazyListState().also { state = it }
+ ) {
+ items((0..3).toList()) {
+ Box(Modifier.size(sizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(0)
+ assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx)
+ }
+ }
+
+ @Test
+ fun viewportOffsetsAreCorrectWithContentPadding() {
+ val sizePx = 45
+ val topPaddingPx = 10
+ val bottomPaddingPx = 15
+ val sizeDp = with(rule.density) { sizePx.toDp() }
+ val topPaddingDp = with(rule.density) { topPaddingPx.toDp() }
+ val bottomPaddingDp = with(rule.density) { bottomPaddingPx.toDp() }
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyColumn(
+ Modifier.size(sizeDp),
+ contentPadding = PaddingValues(top = topPaddingDp, bottom = bottomPaddingDp),
+ state = rememberLazyListState().also { state = it }
+ ) {
+ items((0..3).toList()) {
+ Box(Modifier.size(sizeDp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-topPaddingPx)
+ assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - topPaddingPx)
+ }
+ }
+
+ fun LazyListLayoutInfo.assertVisibleItems(
+ count: Int,
+ startIndex: Int = 0,
+ startOffset: Int = 0,
+ expectedSize: Int = itemSizePx,
+ spacing: Int = 0
+ ) {
+ assertThat(visibleItemsInfo.size).isEqualTo(count)
+ var currentIndex = startIndex
+ var currentOffset = startOffset
+ visibleItemsInfo.forEach {
+ assertThat(it.index).isEqualTo(currentIndex)
+ assertThat(it.offset).isEqualTo(currentOffset)
+ assertThat(it.size).isEqualTo(expectedSize)
+ currentIndex++
+ currentOffset += it.size + spacing
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt
index c77dee6..8b33fbf 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt
@@ -68,11 +68,10 @@
val smallPaddingSize = itemSize / 4
val largePaddingSize = itemSize
rule.setContent {
- LazyColumnFor(
- items = listOf(1),
- state = rememberLazyListState().also { state = it },
+ LazyColumn(
modifier = Modifier.size(containerSize)
.testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it },
contentPadding = PaddingValues(
start = smallPaddingSize,
top = largePaddingSize,
@@ -80,7 +79,9 @@
bottom = largePaddingSize
)
) {
- Spacer(Modifier.fillParentMaxWidth().preferredHeight(itemSize).testTag(ItemTag))
+ items(listOf(1)) {
+ Spacer(Modifier.fillParentMaxWidth().preferredHeight(itemSize).testTag(ItemTag))
+ }
}
}
@@ -101,17 +102,18 @@
fun column_contentPaddingIsNotAffectingScrollPosition() {
lateinit var state: LazyListState
rule.setContent {
- LazyColumnFor(
- items = listOf(1),
- state = rememberLazyListState().also { state = it },
+ LazyColumn(
modifier = Modifier.size(itemSize * 2)
.testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it },
contentPadding = PaddingValues(
top = itemSize,
bottom = itemSize
)
) {
- Spacer(Modifier.fillParentMaxWidth().preferredHeight(itemSize).testTag(ItemTag))
+ items(listOf(1)) {
+ Spacer(Modifier.fillParentMaxWidth().preferredHeight(itemSize).testTag(ItemTag))
+ }
}
}
@@ -127,17 +129,18 @@
lateinit var state: LazyListState
val padding = itemSize * 1.5f
rule.setContent {
- LazyColumnFor(
- items = (0..3).toList(),
- state = rememberLazyListState().also { state = it },
+ LazyColumn(
modifier = Modifier.size(padding * 2 + itemSize)
.testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it },
contentPadding = PaddingValues(
top = padding,
bottom = padding
)
) {
- Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ items((0..3).toList()) {
+ Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ }
}
}
@@ -167,17 +170,18 @@
lateinit var state: LazyListState
val padding = itemSize * 1.5f
rule.setContent {
- LazyColumnFor(
- items = (0..3).toList(),
- state = rememberLazyListState().also { state = it },
+ LazyColumn(
modifier = Modifier.size(itemSize + padding * 2)
.testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it },
contentPadding = PaddingValues(
top = padding,
bottom = padding
)
) {
- Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ items((0..3).toList()) {
+ Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ }
}
}
@@ -201,17 +205,18 @@
lateinit var state: LazyListState
val padding = itemSize * 1.5f
rule.setContent {
- LazyColumnFor(
- items = (0..3).toList(),
- state = rememberLazyListState().also { state = it },
+ LazyColumn(
modifier = Modifier.size(padding * 2 + itemSize)
.testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it },
contentPadding = PaddingValues(
top = padding,
bottom = padding
)
) {
- Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ items((0..3).toList()) {
+ Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ }
}
}
@@ -244,17 +249,18 @@
lateinit var state: LazyListState
val padding = itemSize * 1.5f
rule.setContent {
- LazyColumnFor(
- items = (0..3).toList(),
- state = rememberLazyListState().also { state = it },
+ LazyColumn(
modifier = Modifier.size(padding * 2 + itemSize)
.testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it },
contentPadding = PaddingValues(
top = padding,
bottom = padding
)
) {
- Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ items((0..3).toList()) {
+ Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ }
}
}
@@ -275,8 +281,7 @@
fun column_contentPaddingAndWrapContent() {
rule.setContent {
Box(modifier = Modifier.testTag(ContainerTag)) {
- LazyColumnFor(
- items = listOf(1),
+ LazyColumn(
contentPadding = PaddingValues(
start = 2.dp,
top = 4.dp,
@@ -284,7 +289,9 @@
bottom = 8.dp
)
) {
- Spacer(Modifier.size(itemSize).testTag(ItemTag))
+ items(listOf(1)) {
+ Spacer(Modifier.size(itemSize).testTag(ItemTag))
+ }
}
}
}
@@ -306,8 +313,7 @@
fun column_contentPaddingAndNoContent() {
rule.setContent {
Box(modifier = Modifier.testTag(ContainerTag)) {
- LazyColumnFor(
- items = listOf(0),
+ LazyColumn(
contentPadding = PaddingValues(
start = 2.dp,
top = 4.dp,
@@ -315,6 +321,8 @@
bottom = 8.dp
)
) {
+ items(listOf(0)) {
+ }
}
}
}
@@ -333,11 +341,10 @@
val smallPaddingSize = itemSize / 4
val largePaddingSize = itemSize
rule.setContent {
- LazyRowFor(
- items = listOf(1),
- state = rememberLazyListState().also { state = it },
+ LazyRow(
modifier = Modifier.size(containerSize)
.testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it },
contentPadding = PaddingValues(
top = smallPaddingSize,
start = largePaddingSize,
@@ -345,7 +352,9 @@
end = largePaddingSize
)
) {
- Spacer(Modifier.fillParentMaxHeight().preferredWidth(itemSize).testTag(ItemTag))
+ items(listOf(1)) {
+ Spacer(Modifier.fillParentMaxHeight().preferredWidth(itemSize).testTag(ItemTag))
+ }
}
}
@@ -369,17 +378,18 @@
50.dp.toIntPx().toDp()
}
rule.setContent {
- LazyRowFor(
- items = listOf(1),
- state = rememberLazyListState().also { state = it },
+ LazyRow(
modifier = Modifier.size(itemSize * 2)
.testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it },
contentPadding = PaddingValues(
start = itemSize,
end = itemSize
)
) {
- Spacer(Modifier.fillParentMaxHeight().preferredWidth(itemSize).testTag(ItemTag))
+ items(listOf(1)) {
+ Spacer(Modifier.fillParentMaxHeight().preferredWidth(itemSize).testTag(ItemTag))
+ }
}
}
@@ -395,17 +405,18 @@
lateinit var state: LazyListState
val padding = itemSize * 1.5f
rule.setContent {
- LazyRowFor(
- items = (0..3).toList(),
- state = rememberLazyListState().also { state = it },
+ LazyRow(
modifier = Modifier.size(padding * 2 + itemSize)
.testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it },
contentPadding = PaddingValues(
start = padding,
end = padding
)
) {
- Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ items((0..3).toList()) {
+ Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ }
}
}
@@ -435,17 +446,18 @@
lateinit var state: LazyListState
val padding = itemSize * 1.5f
rule.setContent {
- LazyRowFor(
- items = (0..3).toList(),
- state = rememberLazyListState().also { state = it },
+ LazyRow(
modifier = Modifier.size(itemSize + padding * 2)
.testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it },
contentPadding = PaddingValues(
start = padding,
end = padding
)
) {
- Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ items((0..3).toList()) {
+ Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ }
}
}
@@ -469,17 +481,18 @@
lateinit var state: LazyListState
val padding = itemSize * 1.5f
rule.setContent {
- LazyRowFor(
- items = (0..3).toList(),
- state = rememberLazyListState().also { state = it },
+ LazyRow(
modifier = Modifier.size(padding * 2 + itemSize)
.testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it },
contentPadding = PaddingValues(
start = padding,
end = padding
)
) {
- Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ items((0..3).toList()) {
+ Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ }
}
}
@@ -512,17 +525,18 @@
lateinit var state: LazyListState
val padding = itemSize * 1.5f
rule.setContent {
- LazyRowFor(
- items = (0..3).toList(),
- state = rememberLazyListState().also { state = it },
+ LazyRow(
modifier = Modifier.size(padding * 2 + itemSize)
.testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it },
contentPadding = PaddingValues(
start = padding,
end = padding
)
) {
- Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ items((0..3).toList()) {
+ Spacer(Modifier.size(itemSize).testTag(it.toString()))
+ }
}
}
@@ -543,8 +557,7 @@
fun row_contentPaddingAndWrapContent() {
rule.setContent {
Box(modifier = Modifier.testTag(ContainerTag)) {
- LazyRowFor(
- items = listOf(1),
+ LazyRow(
contentPadding = PaddingValues(
start = 2.dp,
top = 4.dp,
@@ -552,7 +565,9 @@
bottom = 8.dp
)
) {
- Spacer(Modifier.size(itemSize).testTag(ItemTag))
+ items(listOf(1)) {
+ Spacer(Modifier.size(itemSize).testTag(ItemTag))
+ }
}
}
}
@@ -574,8 +589,7 @@
fun row_contentPaddingAndNoContent() {
rule.setContent {
Box(modifier = Modifier.testTag(ContainerTag)) {
- LazyRowFor(
- items = listOf(0),
+ LazyRow(
contentPadding = PaddingValues(
start = 2.dp,
top = 4.dp,
@@ -583,6 +597,7 @@
bottom = 8.dp
)
) {
+ items(listOf(0)) {}
}
}
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsIndexedTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsIndexedTest.kt
new file mode 100644
index 0000000..16bb089
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsIndexedTest.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.compose.foundation.lazy
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.preferredHeight
+import androidx.compose.foundation.layout.preferredWidth
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+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 org.junit.Rule
+import org.junit.Test
+
+class LazyListsIndexedTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun lazyColumnShowsIndexedItems() {
+ val items = (1..4).map { it.toString() }
+
+ rule.setContent {
+ LazyColumn(Modifier.preferredHeight(200.dp)) {
+ itemsIndexed(items) { index, item ->
+ Spacer(
+ Modifier.preferredHeight(101.dp).fillParentMaxWidth()
+ .testTag("$index-$item")
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0-1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("1-2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2-3")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("3-4")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun columnWithIndexesComposedWithCorrectIndexAndItem() {
+ val items = (0..1).map { it.toString() }
+
+ rule.setContent {
+ LazyColumn(Modifier.preferredHeight(200.dp)) {
+ itemsIndexed(items) { index, item ->
+ BasicText("${index}x$item", Modifier.fillParentMaxWidth().height(100.dp))
+ }
+ }
+ }
+
+ rule.onNodeWithText("0x0")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithText("1x1")
+ .assertTopPositionInRootIsEqualTo(100.dp)
+ }
+
+ @Test
+ fun lazyRowShowsIndexedItems() {
+ val items = (1..4).map { it.toString() }
+
+ rule.setContent {
+ LazyRow(Modifier.preferredWidth(200.dp)) {
+ itemsIndexed(items) { index, item ->
+ Spacer(
+ Modifier.preferredWidth(101.dp).fillParentMaxHeight()
+ .testTag("$index-$item")
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0-1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("1-2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2-3")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("3-4")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun rowWithIndexesComposedWithCorrectIndexAndItem() {
+ val items = (0..1).map { it.toString() }
+
+ rule.setContent {
+ LazyRow(Modifier.preferredWidth(200.dp)) {
+ itemsIndexed(items) { index, item ->
+ BasicText("${index}x$item", Modifier.fillParentMaxHeight().width(100.dp))
+ }
+ }
+ }
+
+ rule.onNodeWithText("0x0")
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+
+ rule.onNodeWithText("1x1")
+ .assertLeftPositionInRootIsEqualTo(100.dp)
+ }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsReverseLayoutTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsReverseLayoutTest.kt
new file mode 100644
index 0000000..9f02efa
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsReverseLayoutTest.kt
@@ -0,0 +1,444 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.InternalLayoutApi
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Providers
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.AmbientLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+@OptIn(InternalLayoutApi::class)
+class LazyListsReverseLayoutTest {
+
+ private val ContainerTag = "ContainerTag"
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private var itemSize: Dp = Dp.Infinity
+
+ @Before
+ fun before() {
+ with(rule.density) {
+ itemSize = 50.toDp()
+ }
+ }
+
+ @Test
+ fun column_emitTwoElementsAsOneItem_positionedReversed() {
+ rule.setContent {
+ LazyColumn(
+ reverseLayout = true
+ ) {
+ item {
+ Box(Modifier.size(itemSize).testTag("0"))
+ Box(Modifier.size(itemSize).testTag("1"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun column_emitTwoItems_positionedReversed() {
+ rule.setContent {
+ LazyColumn(
+ reverseLayout = true
+ ) {
+ item {
+ Box(Modifier.size(itemSize).testTag("0"))
+ }
+ item {
+ Box(Modifier.size(itemSize).testTag("1"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun column_initialScrollPositionIs0() {
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyColumn(
+ reverseLayout = true,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun column_scrollInWrongDirectionDoesNothing() {
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyColumn(
+ reverseLayout = true,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ // we scroll down and as the scrolling is reversed it shouldn't affect anything
+ rule.onNodeWithTag(ContainerTag)
+ .scrollBy(y = itemSize, density = rule.density)
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ }
+
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun column_scrollForwardHalfWay() {
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyColumn(
+ reverseLayout = true,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ContainerTag)
+ .scrollBy(y = -itemSize * 0.5f, density = rule.density)
+
+ val scrolled = rule.runOnIdle {
+ assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+ }
+
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(-itemSize + scrolled)
+ rule.onNodeWithTag("1")
+ .assertTopPositionInRootIsEqualTo(scrolled)
+ rule.onNodeWithTag("0")
+ .assertTopPositionInRootIsEqualTo(itemSize + scrolled)
+ }
+
+ @Test
+ fun column_scrollForwardTillTheEnd() {
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyColumn(
+ reverseLayout = true,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+ ) {
+ items((0..3).toList()) {
+ Box(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ // we scroll a bit more than it is possible just to make sure we would stop correctly
+ rule.onNodeWithTag(ContainerTag)
+ .scrollBy(y = -itemSize * 2.2f, density = rule.density)
+
+ rule.runOnIdle {
+ with(rule.density) {
+ val realOffset = state.firstVisibleItemScrollOffset.toDp() +
+ itemSize * state.firstVisibleItemIndex
+ assertThat(realOffset).isEqualTo(itemSize * 2)
+ }
+ }
+
+ rule.onNodeWithTag("3")
+ .assertTopPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("2")
+ .assertTopPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun row_emitTwoElementsAsOneItem_positionedReversed() {
+ rule.setContent {
+ LazyRow(
+ reverseLayout = true
+ ) {
+ item {
+ Box(Modifier.size(itemSize).testTag("0"))
+ Box(Modifier.size(itemSize).testTag("1"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun row_emitTwoItems_positionedReversed() {
+ rule.setContent {
+ LazyRow(
+ reverseLayout = true
+ ) {
+ item {
+ Box(Modifier.size(itemSize).testTag("0"))
+ }
+ item {
+ Box(Modifier.size(itemSize).testTag("1"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun row_initialScrollPositionIs0() {
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyRow(
+ reverseLayout = true,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun row_scrollInWrongDirectionDoesNothing() {
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyRow(
+ reverseLayout = true,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ // we scroll down and as the scrolling is reversed it shouldn't affect anything
+ rule.onNodeWithTag(ContainerTag)
+ .scrollBy(x = itemSize, density = rule.density)
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ }
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun row_scrollForwardHalfWay() {
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyRow(
+ reverseLayout = true,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ContainerTag)
+ .scrollBy(x = -itemSize * 0.5f, density = rule.density)
+
+ val scrolled = rule.runOnIdle {
+ assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+ }
+
+ rule.onNodeWithTag("2")
+ .assertLeftPositionInRootIsEqualTo(-itemSize + scrolled)
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(scrolled)
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(itemSize + scrolled)
+ }
+
+ @Test
+ fun row_scrollForwardTillTheEnd() {
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyRow(
+ reverseLayout = true,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+ ) {
+ items((0..3).toList()) {
+ Box(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+
+ // we scroll a bit more than it is possible just to make sure we would stop correctly
+ rule.onNodeWithTag(ContainerTag)
+ .scrollBy(x = -itemSize * 2.2f, density = rule.density)
+
+ rule.runOnIdle {
+ with(rule.density) {
+ val realOffset = state.firstVisibleItemScrollOffset.toDp() +
+ itemSize * state.firstVisibleItemIndex
+ assertThat(realOffset).isEqualTo(itemSize * 2)
+ }
+ }
+
+ rule.onNodeWithTag("3")
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ rule.onNodeWithTag("2")
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ }
+
+ @Test
+ fun row_rtl_emitTwoElementsAsOneItem_positionedReversed() {
+ rule.setContent {
+ Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
+ LazyRow(
+ reverseLayout = true
+ ) {
+ item {
+ Box(Modifier.size(itemSize).testTag("0"))
+ Box(Modifier.size(itemSize).testTag("1"))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ }
+
+ @Test
+ fun row_rtl_emitTwoItems_positionedReversed() {
+ rule.setContent {
+ Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
+ LazyRow(
+ reverseLayout = true
+ ) {
+ item {
+ Box(Modifier.size(itemSize).testTag("0"))
+ }
+ item {
+ Box(Modifier.size(itemSize).testTag("1"))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(itemSize)
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(0.dp)
+ }
+
+ @Test
+ fun row_rtl_scrollForwardHalfWay() {
+ lateinit var state: LazyListState
+ rule.setContent {
+ Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
+ LazyRow(
+ reverseLayout = true,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.size(itemSize * 2).testTag(ContainerTag)
+ ) {
+ items((0..2).toList()) {
+ Box(Modifier.size(itemSize).testTag("$it"))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(ContainerTag)
+ .scrollBy(x = itemSize * 0.5f, density = rule.density)
+
+ val scrolled = rule.runOnIdle {
+ assertThat(state.firstVisibleItemScrollOffset).isGreaterThan(0)
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ with(rule.density) { state.firstVisibleItemScrollOffset.toDp() }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertLeftPositionInRootIsEqualTo(-scrolled)
+ rule.onNodeWithTag("1")
+ .assertLeftPositionInRootIsEqualTo(itemSize - scrolled)
+ rule.onNodeWithTag("2")
+ .assertLeftPositionInRootIsEqualTo(itemSize * 2 - scrolled)
+ }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyNestedScrollingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyNestedScrollingTest.kt
index 6b5902a..fca6c58 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyNestedScrollingTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyNestedScrollingTest.kt
@@ -16,12 +16,14 @@
package androidx.compose.foundation.lazy
-import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberScrollableController
+import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.gesture.TouchSlop
import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.down
@@ -34,6 +36,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth
+import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -46,21 +49,33 @@
@get:Rule
val rule = createComposeRule()
+ var expectedDragOffset = Float.MAX_VALUE
+
+ @Before
+ fun test() {
+ expectedDragOffset = with(rule.density) {
+ TouchSlop.toPx() + 20
+ }
+ }
+
@Test
fun column_nestedScrollingBackwardInitially() {
val items = (1..3).toList()
var draggedOffset = 0f
rule.setContent {
Box(
- Modifier.draggable(Orientation.Vertical) {
- draggedOffset += it
- }
+ Modifier.scrollable(
+ orientation = Orientation.Vertical,
+ controller = rememberScrollableController {
+ draggedOffset += it
+ it
+ }
+ )
) {
- LazyColumnFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyTag)
- ) {
- Spacer(Modifier.size(50.dp).testTag("$it"))
+ LazyColumn(Modifier.size(100.dp).testTag(LazyTag)) {
+ items(items) {
+ Spacer(Modifier.size(50.dp).testTag("$it"))
+ }
}
}
}
@@ -83,15 +98,18 @@
var draggedOffset = 0f
rule.setContent {
Box(
- Modifier.draggable(Orientation.Vertical) {
- draggedOffset += it
- }
+ Modifier.scrollable(
+ orientation = Orientation.Vertical,
+ controller = rememberScrollableController {
+ draggedOffset += it
+ it
+ }
+ )
) {
- LazyColumnFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyTag)
- ) {
- Spacer(Modifier.size(50.dp).testTag("$it"))
+ LazyColumn(Modifier.size(100.dp).testTag(LazyTag)) {
+ items(items) {
+ Spacer(Modifier.size(50.dp).testTag("$it"))
+ }
}
}
}
@@ -109,12 +127,12 @@
.performGesture {
draggedOffset = 0f
down(Offset(x = 10f, y = 10f))
- moveBy(Offset(x = 0f, y = 50f))
+ moveBy(Offset(x = 0f, y = expectedDragOffset))
up()
}
rule.runOnIdle {
- Truth.assertThat(draggedOffset).isEqualTo(50f)
+ Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset)
}
}
@@ -124,15 +142,18 @@
var draggedOffset = 0f
rule.setContent {
Box(
- Modifier.draggable(Orientation.Vertical) {
- draggedOffset += it
- }
+ Modifier.scrollable(
+ orientation = Orientation.Vertical,
+ controller = rememberScrollableController {
+ draggedOffset += it
+ it
+ }
+ )
) {
- LazyColumnFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyTag)
- ) {
- Spacer(Modifier.size(40.dp).testTag("$it"))
+ LazyColumn(Modifier.size(100.dp).testTag(LazyTag)) {
+ items(items) {
+ Spacer(Modifier.size(40.dp).testTag("$it"))
+ }
}
}
}
@@ -140,12 +161,12 @@
rule.onNodeWithTag(LazyTag)
.performGesture {
down(Offset(x = 10f, y = 10f))
- moveBy(Offset(x = 0f, y = -50f))
+ moveBy(Offset(x = 0f, y = -expectedDragOffset))
up()
}
rule.runOnIdle {
- Truth.assertThat(draggedOffset).isEqualTo(-50f)
+ Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
}
}
@@ -155,15 +176,18 @@
var draggedOffset = 0f
rule.setContent {
Box(
- Modifier.draggable(Orientation.Vertical) {
- draggedOffset += it
- }
+ Modifier.scrollable(
+ orientation = Orientation.Vertical,
+ controller = rememberScrollableController {
+ draggedOffset += it
+ it
+ }
+ )
) {
- LazyColumnFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyTag)
- ) {
- Spacer(Modifier.size(50.dp).testTag("$it"))
+ LazyColumn(Modifier.size(100.dp).testTag(LazyTag)) {
+ items(items) {
+ Spacer(Modifier.size(50.dp).testTag("$it"))
+ }
}
}
}
@@ -176,12 +200,12 @@
.performGesture {
draggedOffset = 0f
down(Offset(x = 10f, y = 10f))
- moveBy(Offset(x = 0f, y = -50f))
+ moveBy(Offset(x = 0f, y = -expectedDragOffset))
up()
}
rule.runOnIdle {
- Truth.assertThat(draggedOffset).isEqualTo(-50f)
+ Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
}
}
@@ -191,15 +215,20 @@
var draggedOffset = 0f
rule.setContent {
Box(
- Modifier.draggable(Orientation.Horizontal) {
- draggedOffset += it
- }
+ Modifier.scrollable(
+ orientation = Orientation.Horizontal,
+ controller = rememberScrollableController {
+ draggedOffset += it
+ it
+ }
+ )
) {
- LazyRowFor(
- items = items,
+ LazyRow(
modifier = Modifier.size(100.dp).testTag(LazyTag)
) {
- Spacer(Modifier.size(50.dp).testTag("$it"))
+ items(items) {
+ Spacer(Modifier.size(50.dp).testTag("$it"))
+ }
}
}
}
@@ -207,12 +236,12 @@
rule.onNodeWithTag(LazyTag)
.performGesture {
down(Offset(x = 10f, y = 10f))
- moveBy(Offset(x = 100f, y = 0f))
+ moveBy(Offset(x = expectedDragOffset, y = 0f))
up()
}
rule.runOnIdle {
- Truth.assertThat(draggedOffset).isEqualTo(100f)
+ Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset)
}
}
@@ -222,15 +251,20 @@
var draggedOffset = 0f
rule.setContent {
Box(
- Modifier.draggable(Orientation.Horizontal) {
- draggedOffset += it
- }
+ Modifier.scrollable(
+ orientation = Orientation.Horizontal,
+ controller = rememberScrollableController {
+ draggedOffset += it
+ it
+ }
+ )
) {
- LazyRowFor(
- items = items,
+ LazyRow(
modifier = Modifier.size(100.dp).testTag(LazyTag)
) {
- Spacer(Modifier.size(50.dp).testTag("$it"))
+ items(items) {
+ Spacer(Modifier.size(50.dp).testTag("$it"))
+ }
}
}
}
@@ -248,12 +282,12 @@
.performGesture {
draggedOffset = 0f
down(Offset(x = 10f, y = 10f))
- moveBy(Offset(x = 50f, y = 0f))
+ moveBy(Offset(x = expectedDragOffset, y = 0f))
up()
}
rule.runOnIdle {
- Truth.assertThat(draggedOffset).isEqualTo(50f)
+ Truth.assertThat(draggedOffset).isEqualTo(expectedDragOffset)
}
}
@@ -263,15 +297,20 @@
var draggedOffset = 0f
rule.setContent {
Box(
- Modifier.draggable(Orientation.Horizontal) {
- draggedOffset += it
- }
+ Modifier.scrollable(
+ orientation = Orientation.Horizontal,
+ controller = rememberScrollableController {
+ draggedOffset += it
+ it
+ }
+ )
) {
- LazyRowFor(
- items = items,
+ LazyRow(
modifier = Modifier.size(100.dp).testTag(LazyTag)
) {
- Spacer(Modifier.size(40.dp).testTag("$it"))
+ items(items) {
+ Spacer(Modifier.size(40.dp).testTag("$it"))
+ }
}
}
}
@@ -279,12 +318,12 @@
rule.onNodeWithTag(LazyTag)
.performGesture {
down(Offset(x = 10f, y = 10f))
- moveBy(Offset(x = -50f, y = 0f))
+ moveBy(Offset(x = -expectedDragOffset, y = 0f))
up()
}
rule.runOnIdle {
- Truth.assertThat(draggedOffset).isEqualTo(-50f)
+ Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
}
}
@@ -294,15 +333,20 @@
var draggedOffset = 0f
rule.setContent {
Box(
- Modifier.draggable(Orientation.Horizontal) {
- draggedOffset += it
- }
+ Modifier.scrollable(
+ orientation = Orientation.Horizontal,
+ controller = rememberScrollableController {
+ draggedOffset += it
+ it
+ }
+ )
) {
- LazyRowFor(
- items = items,
+ LazyRow(
modifier = Modifier.size(100.dp).testTag(LazyTag)
) {
- Spacer(Modifier.size(50.dp).testTag("$it"))
+ items(items) {
+ Spacer(Modifier.size(50.dp).testTag("$it"))
+ }
}
}
}
@@ -315,12 +359,12 @@
.performGesture {
draggedOffset = 0f
down(Offset(x = 10f, y = 10f))
- moveBy(Offset(x = -50f, y = 0f))
+ moveBy(Offset(x = -expectedDragOffset, y = 0f))
up()
}
rule.runOnIdle {
- Truth.assertThat(draggedOffset).isEqualTo(-50f)
+ Truth.assertThat(draggedOffset).isEqualTo(-expectedDragOffset)
}
}
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowForTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowForTest.kt
deleted file mode 100644
index f4d3fd4..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowForTest.kt
+++ /dev/null
@@ -1,903 +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.compose.foundation.lazy
-
-import androidx.compose.animation.core.snap
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.preferredSize
-import androidx.compose.foundation.layout.preferredWidth
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.Providers
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.platform.AmbientLayoutDirection
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsEqualTo
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.getUnclippedBoundsInRoot
-import androidx.compose.ui.test.junit4.StateRestorationTester
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.LayoutDirection
-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 com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.runBlocking
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class LazyRowForTest {
- private val LazyRowForTag = "LazyRowForTag"
-
- @get:Rule
- val rule = createComposeRule()
-
- @Test
- fun lazyRowOnlyVisibleItemsAdded() {
- val items = (1..4).map { it.toString() }
-
- rule.setContent {
- Box(Modifier.preferredWidth(200.dp)) {
- LazyRowFor(items) {
- Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
- }
- }
- }
-
- rule.onNodeWithTag("1")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("2")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("3")
- .assertDoesNotExist()
-
- rule.onNodeWithTag("4")
- .assertDoesNotExist()
- }
-
- @Test
- fun lazyRowScrollToShowItems123() {
- val items = (1..4).map { it.toString() }
-
- rule.setContent {
- Box(Modifier.preferredWidth(200.dp)) {
- LazyRowFor(items, Modifier.testTag(LazyRowForTag)) {
- Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
- }
- }
- }
-
- rule.onNodeWithTag(LazyRowForTag)
- .scrollBy(x = 50.dp, density = rule.density)
-
- rule.onNodeWithTag("1")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("2")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("3")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("4")
- .assertDoesNotExist()
- }
-
- @Test
- fun lazyRowScrollToHideFirstItem() {
- val items = (1..4).map { it.toString() }
-
- rule.setContent {
- Box(Modifier.preferredWidth(200.dp)) {
- LazyRowFor(items, Modifier.testTag(LazyRowForTag)) {
- Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
- }
- }
- }
-
- rule.onNodeWithTag(LazyRowForTag)
- .scrollBy(x = 102.dp, density = rule.density)
-
- rule.onNodeWithTag("1")
- .assertDoesNotExist()
-
- rule.onNodeWithTag("2")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("3")
- .assertIsDisplayed()
- }
-
- @Test
- fun lazyRowScrollToShowItems234() {
- val items = (1..4).map { it.toString() }
-
- rule.setContent {
- Box(Modifier.preferredWidth(200.dp)) {
- LazyRowFor(items, Modifier.testTag(LazyRowForTag)) {
- Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
- }
- }
- }
-
- rule.onNodeWithTag(LazyRowForTag)
- .scrollBy(x = 150.dp, density = rule.density)
-
- rule.onNodeWithTag("1")
- .assertDoesNotExist()
-
- rule.onNodeWithTag("2")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("3")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("4")
- .assertIsDisplayed()
- }
-
- @Test
- fun lazyRowWrapsContent() = with(rule.density) {
- val itemInsideLazyRow = "itemInsideLazyRow"
- val itemOutsideLazyRow = "itemOutsideLazyRow"
- var sameSizeItems by mutableStateOf(true)
-
- rule.setContent {
- Column {
- LazyRowFor(
- items = listOf(1, 2),
- modifier = Modifier.testTag(LazyRowForTag)
- ) {
- if (it == 1) {
- Spacer(Modifier.preferredSize(50.dp).testTag(itemInsideLazyRow))
- } else {
- Spacer(Modifier.preferredSize(if (sameSizeItems) 50.dp else 70.dp))
- }
- }
- Spacer(Modifier.preferredSize(50.dp).testTag(itemOutsideLazyRow))
- }
- }
-
- rule.onNodeWithTag(itemInsideLazyRow)
- .assertIsDisplayed()
-
- rule.onNodeWithTag(itemOutsideLazyRow)
- .assertIsDisplayed()
-
- var lazyRowBounds = rule.onNodeWithTag(LazyRowForTag)
- .getUnclippedBoundsInRoot()
-
- assertThat(lazyRowBounds.left.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
- assertThat(lazyRowBounds.right.toIntPx()).isWithin1PixelFrom(100.dp.toIntPx())
- assertThat(lazyRowBounds.top.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
- assertThat(lazyRowBounds.bottom.toIntPx()).isWithin1PixelFrom(50.dp.toIntPx())
-
- rule.runOnIdle {
- sameSizeItems = false
- }
-
- rule.waitForIdle()
-
- rule.onNodeWithTag(itemInsideLazyRow)
- .assertIsDisplayed()
-
- rule.onNodeWithTag(itemOutsideLazyRow)
- .assertIsDisplayed()
-
- lazyRowBounds = rule.onNodeWithTag(LazyRowForTag)
- .getUnclippedBoundsInRoot()
-
- assertThat(lazyRowBounds.left.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
- assertThat(lazyRowBounds.right.toIntPx()).isWithin1PixelFrom(120.dp.toIntPx())
- assertThat(lazyRowBounds.top.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
- assertThat(lazyRowBounds.bottom.toIntPx()).isWithin1PixelFrom(70.dp.toIntPx())
- }
-
- private val firstItemTag = "firstItemTag"
- private val secondItemTag = "secondItemTag"
-
- private fun prepareLazyRowForAlignment(verticalGravity: Alignment.Vertical) {
- rule.setContent {
- LazyRowFor(
- items = listOf(1, 2),
- modifier = Modifier.testTag(LazyRowForTag).height(100.dp),
- verticalAlignment = verticalGravity
- ) {
- if (it == 1) {
- Spacer(Modifier.preferredSize(50.dp).testTag(firstItemTag))
- } else {
- Spacer(Modifier.preferredSize(70.dp).testTag(secondItemTag))
- }
- }
- }
-
- rule.onNodeWithTag(firstItemTag)
- .assertIsDisplayed()
-
- rule.onNodeWithTag(secondItemTag)
- .assertIsDisplayed()
-
- val lazyRowBounds = rule.onNodeWithTag(LazyRowForTag)
- .getUnclippedBoundsInRoot()
-
- with(rule.density) {
- // Verify the height of the row
- assertThat(lazyRowBounds.top.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
- assertThat(lazyRowBounds.bottom.toIntPx()).isWithin1PixelFrom(100.dp.toIntPx())
- }
- }
-
- @Test
- fun lazyRowAlignmentCenterVertically() {
- prepareLazyRowForAlignment(Alignment.CenterVertically)
-
- rule.onNodeWithTag(firstItemTag)
- .assertPositionInRootIsEqualTo(0.dp, 25.dp)
-
- rule.onNodeWithTag(secondItemTag)
- .assertPositionInRootIsEqualTo(50.dp, 15.dp)
- }
-
- @Test
- fun lazyRowAlignmentTop() {
- prepareLazyRowForAlignment(Alignment.Top)
-
- rule.onNodeWithTag(firstItemTag)
- .assertPositionInRootIsEqualTo(0.dp, 0.dp)
-
- rule.onNodeWithTag(secondItemTag)
- .assertPositionInRootIsEqualTo(50.dp, 0.dp)
- }
-
- @Test
- fun lazyRowAlignmentBottom() {
- prepareLazyRowForAlignment(Alignment.Bottom)
-
- rule.onNodeWithTag(firstItemTag)
- .assertPositionInRootIsEqualTo(0.dp, 50.dp)
-
- rule.onNodeWithTag(secondItemTag)
- .assertPositionInRootIsEqualTo(50.dp, 30.dp)
- }
-
- @Test
- fun itemFillingParentWidth() {
- rule.setContent {
- LazyRowFor(
- items = listOf(0),
- modifier = Modifier.size(width = 100.dp, height = 150.dp)
- ) {
- Spacer(Modifier.fillParentMaxWidth().height(50.dp).testTag(firstItemTag))
- }
- }
-
- rule.onNodeWithTag(firstItemTag)
- .assertWidthIsEqualTo(100.dp)
- .assertHeightIsEqualTo(50.dp)
- }
-
- @Test
- fun itemFillingParentHeight() {
- rule.setContent {
- LazyRowFor(
- items = listOf(0),
- modifier = Modifier.size(width = 100.dp, height = 150.dp)
- ) {
- Spacer(Modifier.width(50.dp).fillParentMaxHeight().testTag(firstItemTag))
- }
- }
-
- rule.onNodeWithTag(firstItemTag)
- .assertWidthIsEqualTo(50.dp)
- .assertHeightIsEqualTo(150.dp)
- }
-
- @Test
- fun itemFillingParentSize() {
- rule.setContent {
- LazyRowFor(
- items = listOf(0),
- modifier = Modifier.size(width = 100.dp, height = 150.dp)
- ) {
- Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
- }
- }
-
- rule.onNodeWithTag(firstItemTag)
- .assertWidthIsEqualTo(100.dp)
- .assertHeightIsEqualTo(150.dp)
- }
-
- @Test
- fun itemFillingParentWidthFraction() {
- rule.setContent {
- LazyRowFor(
- items = listOf(0),
- modifier = Modifier.size(width = 100.dp, height = 150.dp)
- ) {
- Spacer(Modifier.fillParentMaxWidth(0.7f).height(50.dp).testTag(firstItemTag))
- }
- }
-
- rule.onNodeWithTag(firstItemTag)
- .assertWidthIsEqualTo(70.dp)
- .assertHeightIsEqualTo(50.dp)
- }
-
- @Test
- fun itemFillingParentHeightFraction() {
- rule.setContent {
- LazyRowFor(
- items = listOf(0),
- modifier = Modifier.size(width = 100.dp, height = 150.dp)
- ) {
- Spacer(Modifier.width(50.dp).fillParentMaxHeight(0.3f).testTag(firstItemTag))
- }
- }
-
- rule.onNodeWithTag(firstItemTag)
- .assertWidthIsEqualTo(50.dp)
- .assertHeightIsEqualTo(45.dp)
- }
-
- @Test
- fun itemFillingParentSizeFraction() {
- rule.setContent {
- LazyRowFor(
- items = listOf(0),
- modifier = Modifier.size(width = 100.dp, height = 150.dp)
- ) {
- Spacer(Modifier.fillParentMaxSize(0.5f).testTag(firstItemTag))
- }
- }
-
- rule.onNodeWithTag(firstItemTag)
- .assertWidthIsEqualTo(50.dp)
- .assertHeightIsEqualTo(75.dp)
- }
-
- @Test
- fun itemFillingParentSizeParentResized() {
- var parentSize by mutableStateOf(100.dp)
- rule.setContent {
- LazyRowFor(
- items = listOf(0),
- modifier = Modifier.size(parentSize)
- ) {
- Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
- }
- }
-
- rule.runOnIdle {
- parentSize = 150.dp
- }
-
- rule.onNodeWithTag(firstItemTag)
- .assertWidthIsEqualTo(150.dp)
- .assertHeightIsEqualTo(150.dp)
- }
-
- @Test
- fun scrollsLeftInRtl() {
- val items = (1..4).map { it.toString() }
-
- rule.setContent {
- Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
- Box(Modifier.preferredWidth(100.dp)) {
- LazyRowFor(items, Modifier.testTag(LazyRowForTag)) {
- Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
- }
- }
- }
- }
-
- rule.onNodeWithTag(LazyRowForTag)
- .scrollBy(x = (-150).dp, density = rule.density)
-
- rule.onNodeWithTag("1")
- .assertDoesNotExist()
-
- rule.onNodeWithTag("2")
- .assertIsDisplayed()
- }
-
- @Test
- fun whenNotAnymoreAvailableItemWasDisplayed() {
- var items by mutableStateOf((1..30).toList())
- rule.setContent {
- LazyRowFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyRowForTag)
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- // after scroll we will display items 16-20
- rule.onNodeWithTag(LazyRowForTag)
- .scrollBy(x = 300.dp, density = rule.density)
-
- rule.runOnIdle {
- items = (1..10).toList()
- }
-
- // there is no item 16 anymore so we will just display the last items 6-10
- rule.onNodeWithTag("6")
- .assertLeftPositionIsAlmost(0.dp)
- }
-
- @Test
- fun whenFewDisplayedItemsWereRemoved() {
- var items by mutableStateOf((1..10).toList())
- rule.setContent {
- LazyRowFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyRowForTag)
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- // after scroll we will display items 6-10
- rule.onNodeWithTag(LazyRowForTag)
- .scrollBy(x = 100.dp, density = rule.density)
-
- rule.runOnIdle {
- items = (1..8).toList()
- }
-
- // there are no more items 9 and 10, so we have to scroll back
- rule.onNodeWithTag("4")
- .assertLeftPositionIsAlmost(0.dp)
- }
-
- @Test
- fun whenItemsBecameEmpty() {
- var items by mutableStateOf((1..10).toList())
- rule.setContent {
- LazyRowFor(
- items = items,
- modifier = Modifier.sizeIn(maxHeight = 100.dp).testTag(LazyRowForTag)
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- // after scroll we will display items 2-6
- rule.onNodeWithTag(LazyRowForTag)
- .scrollBy(x = 20.dp, density = rule.density)
-
- rule.runOnIdle {
- items = emptyList()
- }
-
- // there are no more items so the LazyRow is zero sized
- rule.onNodeWithTag(LazyRowForTag)
- .assertWidthIsEqualTo(0.dp)
- .assertHeightIsEqualTo(0.dp)
-
- // and has no children
- rule.onNodeWithTag("1")
- .assertDoesNotExist()
- rule.onNodeWithTag("2")
- .assertDoesNotExist()
- }
-
- @Test
- fun scrollBackAndForth() {
- val items by mutableStateOf((1..20).toList())
- rule.setContent {
- LazyRowFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyRowForTag)
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- // after scroll we will display items 6-10
- rule.onNodeWithTag(LazyRowForTag)
- .scrollBy(x = 100.dp, density = rule.density)
-
- // and scroll back
- rule.onNodeWithTag(LazyRowForTag)
- .scrollBy(x = (-100).dp, density = rule.density)
-
- rule.onNodeWithTag("1")
- .assertLeftPositionIsAlmost(0.dp)
- }
-
- @Test
- fun tryToScrollBackwardWhenAlreadyOnTop() {
- val items by mutableStateOf((1..20).toList())
- rule.setContent {
- LazyRowFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyRowForTag)
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- // we already displaying the first item, so this should do nothing
- rule.onNodeWithTag(LazyRowForTag)
- .scrollBy(x = (-50).dp, density = rule.density)
-
- rule.onNodeWithTag("1")
- .assertLeftPositionIsAlmost(0.dp)
- rule.onNodeWithTag("5")
- .assertLeftPositionIsAlmost(80.dp)
- }
-
- private fun SemanticsNodeInteraction.assertLeftPositionIsAlmost(expected: Dp) {
- getUnclippedBoundsInRoot().left.assertIsEqualTo(expected, tolerance = 1.dp)
- }
-
- @Test
- fun contentOfNotStableItemsIsNotRecomposedDuringScroll() {
- val items = listOf(NotStable(1), NotStable(2))
- var firstItemRecomposed = 0
- var secondItemRecomposed = 0
- rule.setContent {
- LazyRowFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyRowForTag)
- ) {
- if (it.count == 1) {
- firstItemRecomposed++
- } else {
- secondItemRecomposed++
- }
- Spacer(Modifier.size(75.dp))
- }
- }
-
- rule.runOnIdle {
- assertThat(firstItemRecomposed).isEqualTo(1)
- assertThat(secondItemRecomposed).isEqualTo(1)
- }
-
- rule.onNodeWithTag(LazyRowForTag)
- .scrollBy(x = (50).dp, density = rule.density)
-
- rule.runOnIdle {
- assertThat(firstItemRecomposed).isEqualTo(1)
- assertThat(secondItemRecomposed).isEqualTo(1)
- }
- }
-
- @Test
- fun onlyOneMeasurePassForScrollEvent() {
- val items by mutableStateOf((1..20).toList())
- lateinit var state: LazyListState
- rule.setContent {
- state = rememberLazyListState()
- LazyRowFor(
- items = items,
- modifier = Modifier.size(100.dp),
- state = state
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- val initialMeasurePasses = state.numMeasurePasses
-
- rule.runOnIdle {
- with(rule.density) {
- state.onScroll(-110.dp.toPx())
- }
- }
-
- rule.waitForIdle()
-
- assertThat(state.numMeasurePasses).isEqualTo(initialMeasurePasses + 1)
- }
-
- @Test
- fun stateUpdatedAfterScroll() {
- val items by mutableStateOf((1..20).toList())
- lateinit var state: LazyListState
- rule.setContent {
- state = rememberLazyListState()
- LazyRowFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyRowForTag),
- state = state
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
- }
-
- rule.onNodeWithTag(LazyRowForTag)
- .scrollBy(x = 30.dp, density = rule.density)
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(1)
-
- with(rule.density) {
- // TODO(b/169232491): test scrolling doesn't appear to be scrolling exactly the right
- // number of pixels
- val expectedOffset = 10.dp.toIntPx()
- val tolerance = 2.dp.toIntPx()
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset, tolerance)
- }
- }
- }
-
- @Test
- fun stateUpdatedAfterScrollWithinTheSameItem() {
- val items by mutableStateOf((1..20).toList())
- lateinit var state: LazyListState
- rule.setContent {
- state = rememberLazyListState()
- LazyRowFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyRowForTag),
- state = state
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- rule.onNodeWithTag(LazyRowForTag)
- .scrollBy(x = 10.dp, density = rule.density)
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- with(rule.density) {
- val expectedOffset = 10.dp.toIntPx()
- val tolerance = 2.dp.toIntPx()
- assertThat(state.firstVisibleItemScrollOffset)
- .isEqualTo(expectedOffset, tolerance)
- }
- }
- }
-
- @Test
- fun initialScrollIsApplied() {
- val items by mutableStateOf((0..20).toList())
- lateinit var state: LazyListState
- val expectedOffset = with(rule.density) { 10.dp.toIntPx() }
- rule.setContent {
- state = rememberLazyListState(2, expectedOffset)
- LazyRowFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyRowForTag),
- state = state
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(2)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset)
- }
-
- rule.onNodeWithTag("2")
- .assertLeftPositionInRootIsEqualTo((-10).dp)
- }
-
- @Test
- fun stateIsRestored() {
- val restorationTester = StateRestorationTester(rule)
- val items by mutableStateOf((1..20).toList())
- var state: LazyListState? = null
- restorationTester.setContent {
- state = rememberLazyListState()
- LazyRowFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyRowForTag),
- state = state!!
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- rule.onNodeWithTag(LazyRowForTag)
- .scrollBy(x = 30.dp, density = rule.density)
-
- val (index, scrollOffset) = rule.runOnIdle {
- state!!.firstVisibleItemIndex to state!!.firstVisibleItemScrollOffset
- }
-
- state = null
-
- restorationTester.emulateSavedInstanceStateRestore()
-
- rule.runOnIdle {
- assertThat(state!!.firstVisibleItemIndex).isEqualTo(index)
- assertThat(state!!.firstVisibleItemScrollOffset).isEqualTo(scrollOffset)
- }
- }
-
- @Test
- fun snapToItemIndex() {
- val items by mutableStateOf((1..20).toList())
- lateinit var state: LazyListState
- rule.setContent {
- state = rememberLazyListState()
- LazyRowFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyRowForTag),
- state = state
- ) {
- Spacer(Modifier.size(20.dp).testTag("$it"))
- }
- }
-
- rule.runOnIdle {
- runBlocking {
- state.snapToItemIndex(3, 10)
- }
- assertThat(state.firstVisibleItemIndex).isEqualTo(3)
- assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
- }
- }
-
- @Test
- fun itemsAreNotRedrawnDuringScroll() {
- val items = (0..20).toList()
- val redrawCount = Array(6) { 0 }
- rule.setContent {
- LazyRowFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyRowForTag)
- ) {
- Spacer(
- Modifier.size(20.dp)
- .drawBehind { redrawCount[it]++ }
- )
- }
- }
-
- rule.onNodeWithTag(LazyRowForTag)
- .scrollBy(x = 10.dp, density = rule.density)
-
- rule.runOnIdle {
- redrawCount.forEachIndexed { index, i ->
- Truth.assertWithMessage("Item with index $index was redrawn $i times")
- .that(i).isEqualTo(1)
- }
- }
- }
-
- @Test
- fun itemInvalidationIsNotCausingAnotherItemToRedraw() {
- val items = (0..1).toList()
- val redrawCount = Array(2) { 0 }
- var stateUsedInDrawScope by mutableStateOf(false)
- rule.setContent {
- LazyRowFor(
- items = items,
- modifier = Modifier.size(100.dp).testTag(LazyRowForTag)
- ) {
- Spacer(
- Modifier.size(50.dp)
- .drawBehind {
- redrawCount[it]++
- if (it == 1) {
- stateUsedInDrawScope.hashCode()
- }
- }
- )
- }
- }
-
- rule.runOnIdle {
- stateUsedInDrawScope = true
- }
-
- rule.runOnIdle {
- Truth.assertWithMessage("First items is not expected to be redrawn")
- .that(redrawCount[0]).isEqualTo(1)
- Truth.assertWithMessage("Second items is expected to be redrawn")
- .that(redrawCount[1]).isEqualTo(2)
- }
- }
-
- @Test
- fun notVisibleAnymoreItemNotAffectingCrossAxisSize() {
- val items = (0..1).toList()
- val itemSize = with(rule.density) { 30.toDp() }
- val itemSizeMinusOne = with(rule.density) { 29.toDp() }
- lateinit var state: LazyListState
- rule.setContent {
- LazyRowFor(
- items = items,
- state = rememberLazyListState().also { state = it },
- modifier = Modifier.width(itemSizeMinusOne).testTag(LazyRowForTag)
- ) {
- Spacer(
- if (it == 0) {
- Modifier.height(30.dp).width(itemSizeMinusOne)
- } else {
- Modifier.height(20.dp).width(itemSize)
- }
- )
- }
- }
-
- state.scrollBy(itemSize)
-
- rule.onNodeWithTag(LazyRowForTag)
- .assertHeightIsEqualTo(20.dp)
- }
-
- @Test
- fun itemStillVisibleAfterOverscrollIsAffectingCrossAxisSize() {
- val items = (0..2).toList()
- val itemSize = with(rule.density) { 30.toDp() }
- lateinit var state: LazyListState
- rule.setContent {
- LazyRowFor(
- items = items,
- state = rememberLazyListState().also { state = it },
- modifier = Modifier.width(itemSize * 1.75f).testTag(LazyRowForTag)
- ) {
- Spacer(
- if (it == 0) {
- Modifier.height(30.dp).width(itemSize / 2)
- } else if (it == 1) {
- Modifier.height(20.dp).width(itemSize / 2)
- } else {
- Modifier.height(20.dp).width(itemSize)
- }
- )
- }
- }
-
- state.scrollBy(itemSize)
-
- rule.onNodeWithTag(LazyRowForTag)
- .assertHeightIsEqualTo(30.dp)
- }
-
- private fun LazyListState.scrollBy(offset: Dp) {
- runBlocking {
- smoothScrollBy(with(rule.density) { offset.toIntPx().toFloat() }, snap())
- }
- }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowTest.kt
index fe720d7..1ab378a 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowTest.kt
@@ -16,17 +16,44 @@
package androidx.compose.foundation.lazy
+import androidx.compose.animation.core.snap
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.preferredSize
import androidx.compose.foundation.layout.preferredWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Providers
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.platform.AmbientLayoutDirection
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsEqualTo
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
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 com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -34,83 +61,12 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
class LazyRowTest {
- private val LazyRowTag = "LazyRowTag"
+ private val LazyListTag = "LazyListTag"
@get:Rule
val rule = createComposeRule()
@Test
- fun lazyRowShowsItem() {
- val itemTestTag = "itemTestTag"
-
- rule.setContent {
- LazyRow {
- item {
- Spacer(
- Modifier.preferredWidth(10.dp).fillParentMaxHeight().testTag(itemTestTag)
- )
- }
- }
- }
-
- rule.onNodeWithTag(itemTestTag)
- .assertIsDisplayed()
- }
-
- @Test
- fun lazyRowShowsItems() {
- val items = (1..4).map { it.toString() }
-
- rule.setContent {
- LazyRow(Modifier.preferredWidth(200.dp)) {
- items(items) {
- Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
- }
- }
- }
-
- rule.onNodeWithTag("1")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("2")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("3")
- .assertDoesNotExist()
-
- rule.onNodeWithTag("4")
- .assertDoesNotExist()
- }
-
- @Test
- fun lazyRowShowsIndexedItems() {
- val items = (1..4).map { it.toString() }
-
- rule.setContent {
- LazyRow(Modifier.preferredWidth(200.dp)) {
- itemsIndexed(items) { index, item ->
- Spacer(
- Modifier.preferredWidth(101.dp).fillParentMaxHeight()
- .testTag("$index-$item")
- )
- }
- }
- }
-
- rule.onNodeWithTag("0-1")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("1-2")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("2-3")
- .assertDoesNotExist()
-
- rule.onNodeWithTag("3-4")
- .assertDoesNotExist()
- }
-
- @Test
fun lazyRowShowsCombinedItems() {
val itemTestTag = "itemTestTag"
val items = listOf(1, 2).map { it.toString() }
@@ -155,59 +111,6 @@
}
@Test
- fun lazyRowShowsItemsOnScroll() {
- val items = (1..4).map { it.toString() }
-
- rule.setContent {
- LazyRow(Modifier.preferredWidth(200.dp).testTag(LazyRowTag)) {
- items(items) {
- Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
- }
- }
- }
-
- rule.onNodeWithTag(LazyRowTag)
- .scrollBy(x = 50.dp, density = rule.density)
-
- rule.onNodeWithTag("1")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("2")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("3")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("4")
- .assertDoesNotExist()
- }
-
- @Test
- fun lazyRowScrollHidesItem() {
- val items = (1..4).map { it.toString() }
-
- rule.setContent {
- LazyRow(Modifier.preferredWidth(200.dp).testTag(LazyRowTag)) {
- items(items) {
- Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
- }
- }
- }
-
- rule.onNodeWithTag(LazyRowTag)
- .scrollBy(x = 103.dp, density = rule.density)
-
- rule.onNodeWithTag("1")
- .assertDoesNotExist()
-
- rule.onNodeWithTag("2")
- .assertIsDisplayed()
-
- rule.onNodeWithTag("3")
- .assertIsDisplayed()
- }
-
- @Test
fun lazyRowAllowEmptyListItems() {
val itemTag = "itemTag"
@@ -253,4 +156,838 @@
rule.onNodeWithTag("3")
.assertDoesNotExist()
}
-}
\ No newline at end of file
+
+ @Test
+ fun lazyRowOnlyVisibleItemsAdded() {
+ val items = (1..4).map { it.toString() }
+
+ rule.setContent {
+ Box(Modifier.preferredWidth(200.dp)) {
+ LazyRow {
+ items(items) {
+ Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("3")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("4")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun lazyRowScrollToShowItems123() {
+ val items = (1..4).map { it.toString() }
+
+ rule.setContent {
+ Box(Modifier.preferredWidth(200.dp)) {
+ LazyRow(Modifier.testTag(LazyListTag)) {
+ items(items) {
+ Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(x = 50.dp, density = rule.density)
+
+ rule.onNodeWithTag("1")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("3")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("4")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun lazyRowScrollToHideFirstItem() {
+ val items = (1..4).map { it.toString() }
+
+ rule.setContent {
+ Box(Modifier.preferredWidth(200.dp)) {
+ LazyRow(Modifier.testTag(LazyListTag)) {
+ items(items) {
+ Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(x = 102.dp, density = rule.density)
+
+ rule.onNodeWithTag("1")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("3")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun lazyRowScrollToShowItems234() {
+ val items = (1..4).map { it.toString() }
+
+ rule.setContent {
+ Box(Modifier.preferredWidth(200.dp)) {
+ LazyRow(Modifier.testTag(LazyListTag)) {
+ items(items) {
+ Spacer(Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(x = 150.dp, density = rule.density)
+
+ rule.onNodeWithTag("1")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("3")
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag("4")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun lazyRowWrapsContent() = with(rule.density) {
+ val itemInsideLazyRow = "itemInsideLazyRow"
+ val itemOutsideLazyRow = "itemOutsideLazyRow"
+ var sameSizeItems by mutableStateOf(true)
+
+ rule.setContent {
+ Column {
+ LazyRow(Modifier.testTag(LazyListTag)) {
+ items(listOf(1, 2)) {
+ if (it == 1) {
+ Spacer(Modifier.preferredSize(50.dp).testTag(itemInsideLazyRow))
+ } else {
+ Spacer(Modifier.preferredSize(if (sameSizeItems) 50.dp else 70.dp))
+ }
+ }
+ }
+ Spacer(Modifier.preferredSize(50.dp).testTag(itemOutsideLazyRow))
+ }
+ }
+
+ rule.onNodeWithTag(itemInsideLazyRow)
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag(itemOutsideLazyRow)
+ .assertIsDisplayed()
+
+ var lazyRowBounds = rule.onNodeWithTag(LazyListTag)
+ .getUnclippedBoundsInRoot()
+
+ assertThat(lazyRowBounds.left.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
+ assertThat(lazyRowBounds.right.toIntPx()).isWithin1PixelFrom(100.dp.toIntPx())
+ assertThat(lazyRowBounds.top.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
+ assertThat(lazyRowBounds.bottom.toIntPx()).isWithin1PixelFrom(50.dp.toIntPx())
+
+ rule.runOnIdle {
+ sameSizeItems = false
+ }
+
+ rule.waitForIdle()
+
+ rule.onNodeWithTag(itemInsideLazyRow)
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag(itemOutsideLazyRow)
+ .assertIsDisplayed()
+
+ lazyRowBounds = rule.onNodeWithTag(LazyListTag)
+ .getUnclippedBoundsInRoot()
+
+ assertThat(lazyRowBounds.left.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
+ assertThat(lazyRowBounds.right.toIntPx()).isWithin1PixelFrom(120.dp.toIntPx())
+ assertThat(lazyRowBounds.top.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
+ assertThat(lazyRowBounds.bottom.toIntPx()).isWithin1PixelFrom(70.dp.toIntPx())
+ }
+
+ private val firstItemTag = "firstItemTag"
+ private val secondItemTag = "secondItemTag"
+
+ private fun prepareLazyRowForAlignment(verticalGravity: Alignment.Vertical) {
+ rule.setContent {
+ LazyRow(
+ Modifier.testTag(LazyListTag).height(100.dp),
+ verticalAlignment = verticalGravity
+ ) {
+ items(listOf(1, 2)) {
+ if (it == 1) {
+ Spacer(Modifier.preferredSize(50.dp).testTag(firstItemTag))
+ } else {
+ Spacer(Modifier.preferredSize(70.dp).testTag(secondItemTag))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertIsDisplayed()
+
+ rule.onNodeWithTag(secondItemTag)
+ .assertIsDisplayed()
+
+ val lazyRowBounds = rule.onNodeWithTag(LazyListTag)
+ .getUnclippedBoundsInRoot()
+
+ with(rule.density) {
+ // Verify the height of the row
+ assertThat(lazyRowBounds.top.toIntPx()).isWithin1PixelFrom(0.dp.toIntPx())
+ assertThat(lazyRowBounds.bottom.toIntPx()).isWithin1PixelFrom(100.dp.toIntPx())
+ }
+ }
+
+ @Test
+ fun lazyRowAlignmentCenterVertically() {
+ prepareLazyRowForAlignment(Alignment.CenterVertically)
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertPositionInRootIsEqualTo(0.dp, 25.dp)
+
+ rule.onNodeWithTag(secondItemTag)
+ .assertPositionInRootIsEqualTo(50.dp, 15.dp)
+ }
+
+ @Test
+ fun lazyRowAlignmentTop() {
+ prepareLazyRowForAlignment(Alignment.Top)
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+
+ rule.onNodeWithTag(secondItemTag)
+ .assertPositionInRootIsEqualTo(50.dp, 0.dp)
+ }
+
+ @Test
+ fun lazyRowAlignmentBottom() {
+ prepareLazyRowForAlignment(Alignment.Bottom)
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertPositionInRootIsEqualTo(0.dp, 50.dp)
+
+ rule.onNodeWithTag(secondItemTag)
+ .assertPositionInRootIsEqualTo(50.dp, 30.dp)
+ }
+
+ @Test
+ fun itemFillingParentWidth() {
+ rule.setContent {
+ LazyRow(Modifier.size(width = 100.dp, height = 150.dp)) {
+ items(listOf(0)) {
+ Spacer(Modifier.fillParentMaxWidth().height(50.dp).testTag(firstItemTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(100.dp)
+ .assertHeightIsEqualTo(50.dp)
+ }
+
+ @Test
+ fun itemFillingParentHeight() {
+ rule.setContent {
+ LazyRow(Modifier.size(width = 100.dp, height = 150.dp)) {
+ items(listOf(0)) {
+ Spacer(Modifier.width(50.dp).fillParentMaxHeight().testTag(firstItemTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(150.dp)
+ }
+
+ @Test
+ fun itemFillingParentSize() {
+ rule.setContent {
+ LazyRow(Modifier.size(width = 100.dp, height = 150.dp)) {
+ items(listOf(0)) {
+ Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(100.dp)
+ .assertHeightIsEqualTo(150.dp)
+ }
+
+ @Test
+ fun itemFillingParentWidthFraction() {
+ rule.setContent {
+ LazyRow(Modifier.size(width = 100.dp, height = 150.dp)) {
+ items(listOf(0)) {
+ Spacer(Modifier.fillParentMaxWidth(0.7f).height(50.dp).testTag(firstItemTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(70.dp)
+ .assertHeightIsEqualTo(50.dp)
+ }
+
+ @Test
+ fun itemFillingParentHeightFraction() {
+ rule.setContent {
+ LazyRow(Modifier.size(width = 100.dp, height = 150.dp)) {
+ items(listOf(0)) {
+ Spacer(Modifier.width(50.dp).fillParentMaxHeight(0.3f).testTag(firstItemTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(45.dp)
+ }
+
+ @Test
+ fun itemFillingParentSizeFraction() {
+ rule.setContent {
+ LazyRow(Modifier.size(width = 100.dp, height = 150.dp)) {
+ items(listOf(0)) {
+ Spacer(Modifier.fillParentMaxSize(0.5f).testTag(firstItemTag))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(50.dp)
+ .assertHeightIsEqualTo(75.dp)
+ }
+
+ @Test
+ fun itemFillingParentSizeParentResized() {
+ var parentSize by mutableStateOf(100.dp)
+ rule.setContent {
+ LazyRow(Modifier.size(parentSize)) {
+ items(listOf(0)) {
+ Spacer(Modifier.fillParentMaxSize().testTag(firstItemTag))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ parentSize = 150.dp
+ }
+
+ rule.onNodeWithTag(firstItemTag)
+ .assertWidthIsEqualTo(150.dp)
+ .assertHeightIsEqualTo(150.dp)
+ }
+
+ @Test
+ fun scrollsLeftInRtl() {
+ val items = (1..4).map { it.toString() }
+
+ rule.setContent {
+ Providers(AmbientLayoutDirection provides LayoutDirection.Rtl) {
+ Box(Modifier.preferredWidth(100.dp)) {
+ LazyRow(Modifier.testTag(LazyListTag)) {
+ items(items) {
+ Spacer(
+ Modifier.preferredWidth(101.dp).fillParentMaxHeight().testTag(it)
+ )
+ }
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(x = (-150).dp, density = rule.density)
+
+ rule.onNodeWithTag("1")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("2")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun whenNotAnymoreAvailableItemWasDisplayed() {
+ var items by mutableStateOf((1..30).toList())
+ rule.setContent {
+ LazyRow(Modifier.size(100.dp).testTag(LazyListTag)) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ // after scroll we will display items 16-20
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(x = 300.dp, density = rule.density)
+
+ rule.runOnIdle {
+ items = (1..10).toList()
+ }
+
+ // there is no item 16 anymore so we will just display the last items 6-10
+ rule.onNodeWithTag("6")
+ .assertLeftPositionIsAlmost(0.dp)
+ }
+
+ @Test
+ fun whenFewDisplayedItemsWereRemoved() {
+ var items by mutableStateOf((1..10).toList())
+ rule.setContent {
+ LazyRow(Modifier.size(100.dp).testTag(LazyListTag)) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ // after scroll we will display items 6-10
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(x = 100.dp, density = rule.density)
+
+ rule.runOnIdle {
+ items = (1..8).toList()
+ }
+
+ // there are no more items 9 and 10, so we have to scroll back
+ rule.onNodeWithTag("4")
+ .assertLeftPositionIsAlmost(0.dp)
+ }
+
+ @Test
+ fun whenItemsBecameEmpty() {
+ var items by mutableStateOf((1..10).toList())
+ rule.setContent {
+ LazyRow(Modifier.sizeIn(maxHeight = 100.dp).testTag(LazyListTag)) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ // after scroll we will display items 2-6
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(x = 20.dp, density = rule.density)
+
+ rule.runOnIdle {
+ items = emptyList()
+ }
+
+ // there are no more items so the LazyRow is zero sized
+ rule.onNodeWithTag(LazyListTag)
+ .assertWidthIsEqualTo(0.dp)
+ .assertHeightIsEqualTo(0.dp)
+
+ // and has no children
+ rule.onNodeWithTag("1")
+ .assertDoesNotExist()
+ rule.onNodeWithTag("2")
+ .assertDoesNotExist()
+ }
+
+ @Test
+ fun scrollBackAndForth() {
+ val items by mutableStateOf((1..20).toList())
+ rule.setContent {
+ LazyRow(Modifier.size(100.dp).testTag(LazyListTag)) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ // after scroll we will display items 6-10
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(x = 100.dp, density = rule.density)
+
+ // and scroll back
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(x = (-100).dp, density = rule.density)
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionIsAlmost(0.dp)
+ }
+
+ @Test
+ fun tryToScrollBackwardWhenAlreadyOnTop() {
+ val items by mutableStateOf((1..20).toList())
+ rule.setContent {
+ LazyRow(Modifier.size(100.dp).testTag(LazyListTag)) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ // we already displaying the first item, so this should do nothing
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(x = (-50).dp, density = rule.density)
+
+ rule.onNodeWithTag("1")
+ .assertLeftPositionIsAlmost(0.dp)
+ rule.onNodeWithTag("5")
+ .assertLeftPositionIsAlmost(80.dp)
+ }
+
+ private fun SemanticsNodeInteraction.assertLeftPositionIsAlmost(expected: Dp) {
+ getUnclippedBoundsInRoot().left.assertIsEqualTo(expected, tolerance = 1.dp)
+ }
+
+ @Test
+ fun contentOfNotStableItemsIsNotRecomposedDuringScroll() {
+ val items = listOf(NotStable(1), NotStable(2))
+ var firstItemRecomposed = 0
+ var secondItemRecomposed = 0
+ rule.setContent {
+ LazyRow(Modifier.size(100.dp).testTag(LazyListTag)) {
+ items(items) {
+ if (it.count == 1) {
+ firstItemRecomposed++
+ } else {
+ secondItemRecomposed++
+ }
+ Spacer(Modifier.size(75.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(firstItemRecomposed).isEqualTo(1)
+ assertThat(secondItemRecomposed).isEqualTo(1)
+ }
+
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(x = (50).dp, density = rule.density)
+
+ rule.runOnIdle {
+ assertThat(firstItemRecomposed).isEqualTo(1)
+ assertThat(secondItemRecomposed).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun onlyOneMeasurePassForScrollEvent() {
+ val items by mutableStateOf((1..20).toList())
+ lateinit var state: LazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ LazyRow(Modifier.size(100.dp), state = state) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ val initialMeasurePasses = state.numMeasurePasses
+
+ rule.runOnIdle {
+ with(rule.density) {
+ state.onScroll(-110.dp.toPx())
+ }
+ }
+
+ rule.waitForIdle()
+
+ assertThat(state.numMeasurePasses).isEqualTo(initialMeasurePasses + 1)
+ }
+
+ @Test
+ fun stateUpdatedAfterScroll() {
+ val items by mutableStateOf((1..20).toList())
+ lateinit var state: LazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ LazyRow(
+ Modifier.size(100.dp).testTag(LazyListTag),
+ state = state
+ ) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+ }
+
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(x = 30.dp, density = rule.density)
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+
+ with(rule.density) {
+ // TODO(b/169232491): test scrolling doesn't appear to be scrolling exactly the right
+ // number of pixels
+ val expectedOffset = 10.dp.toIntPx()
+ val tolerance = 2.dp.toIntPx()
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset, tolerance)
+ }
+ }
+ }
+
+ @Test
+ fun stateUpdatedAfterScrollWithinTheSameItem() {
+ val items by mutableStateOf((1..20).toList())
+ lateinit var state: LazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ LazyRow(
+ Modifier.size(100.dp).testTag(LazyListTag),
+ state = state
+ ) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(x = 10.dp, density = rule.density)
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ with(rule.density) {
+ val expectedOffset = 10.dp.toIntPx()
+ val tolerance = 2.dp.toIntPx()
+ assertThat(state.firstVisibleItemScrollOffset)
+ .isEqualTo(expectedOffset, tolerance)
+ }
+ }
+ }
+
+ @Test
+ fun initialScrollIsApplied() {
+ val items by mutableStateOf((0..20).toList())
+ lateinit var state: LazyListState
+ val expectedOffset = with(rule.density) { 10.dp.toIntPx() }
+ rule.setContent {
+ state = rememberLazyListState(2, expectedOffset)
+ LazyRow(Modifier.size(100.dp).testTag(LazyListTag), state = state) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(2)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(expectedOffset)
+ }
+
+ rule.onNodeWithTag("2")
+ .assertLeftPositionInRootIsEqualTo((-10).dp)
+ }
+
+ @Test
+ fun stateIsRestored() {
+ val restorationTester = StateRestorationTester(rule)
+ val items by mutableStateOf((1..20).toList())
+ var state: LazyListState? = null
+ restorationTester.setContent {
+ state = rememberLazyListState()
+ LazyRow(
+ Modifier.size(100.dp).testTag(LazyListTag),
+ state = state!!
+ ) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(x = 30.dp, density = rule.density)
+
+ val (index, scrollOffset) = rule.runOnIdle {
+ state!!.firstVisibleItemIndex to state!!.firstVisibleItemScrollOffset
+ }
+
+ state = null
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ rule.runOnIdle {
+ assertThat(state!!.firstVisibleItemIndex).isEqualTo(index)
+ assertThat(state!!.firstVisibleItemScrollOffset).isEqualTo(scrollOffset)
+ }
+ }
+
+ @Test
+ fun snapToItemIndex() {
+ val items by mutableStateOf((1..20).toList())
+ lateinit var state: LazyListState
+ rule.setContent {
+ state = rememberLazyListState()
+ LazyRow(
+ Modifier.size(100.dp).testTag(LazyListTag),
+ state = state
+ ) {
+ items(items) {
+ Spacer(Modifier.size(20.dp).testTag("$it"))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ runBlocking {
+ state.snapToItemIndex(3, 10)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(3)
+ assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
+ }
+ }
+
+ @Test
+ fun itemsAreNotRedrawnDuringScroll() {
+ val items = (0..20).toList()
+ val redrawCount = Array(6) { 0 }
+ rule.setContent {
+ LazyRow(Modifier.size(100.dp).testTag(LazyListTag)) {
+ items(items) {
+ Spacer(
+ Modifier.size(20.dp)
+ .drawBehind { redrawCount[it]++ }
+ )
+ }
+ }
+ }
+
+ rule.onNodeWithTag(LazyListTag)
+ .scrollBy(x = 10.dp, density = rule.density)
+
+ rule.runOnIdle {
+ redrawCount.forEachIndexed { index, i ->
+ Truth.assertWithMessage("Item with index $index was redrawn $i times")
+ .that(i).isEqualTo(1)
+ }
+ }
+ }
+
+ @Test
+ fun itemInvalidationIsNotCausingAnotherItemToRedraw() {
+ val items = (0..1).toList()
+ val redrawCount = Array(2) { 0 }
+ var stateUsedInDrawScope by mutableStateOf(false)
+ rule.setContent {
+ LazyRow(Modifier.size(100.dp).testTag(LazyListTag)) {
+ items(items) {
+ Spacer(
+ Modifier.size(50.dp)
+ .drawBehind {
+ redrawCount[it]++
+ if (it == 1) {
+ stateUsedInDrawScope.hashCode()
+ }
+ }
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ stateUsedInDrawScope = true
+ }
+
+ rule.runOnIdle {
+ Truth.assertWithMessage("First items is not expected to be redrawn")
+ .that(redrawCount[0]).isEqualTo(1)
+ Truth.assertWithMessage("Second items is expected to be redrawn")
+ .that(redrawCount[1]).isEqualTo(2)
+ }
+ }
+
+ @Test
+ fun notVisibleAnymoreItemNotAffectingCrossAxisSize() {
+ val items = (0..1).toList()
+ val itemSize = with(rule.density) { 30.toDp() }
+ val itemSizeMinusOne = with(rule.density) { 29.toDp() }
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyRow(
+ Modifier.width(itemSizeMinusOne).testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it }
+ ) {
+ items(items) {
+ Spacer(
+ if (it == 0) {
+ Modifier.height(30.dp).width(itemSizeMinusOne)
+ } else {
+ Modifier.height(20.dp).width(itemSize)
+ }
+ )
+ }
+ }
+ }
+
+ state.scrollBy(itemSize)
+
+ rule.onNodeWithTag(LazyListTag)
+ .assertHeightIsEqualTo(20.dp)
+ }
+
+ @Test
+ fun itemStillVisibleAfterOverscrollIsAffectingCrossAxisSize() {
+ val items = (0..2).toList()
+ val itemSize = with(rule.density) { 30.toDp() }
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyRow(
+ Modifier.width(itemSize * 1.75f).testTag(LazyListTag),
+ state = rememberLazyListState().also { state = it }
+ ) {
+ items(items) {
+ Spacer(
+ if (it == 0) {
+ Modifier.height(30.dp).width(itemSize / 2)
+ } else if (it == 1) {
+ Modifier.height(20.dp).width(itemSize / 2)
+ } else {
+ Modifier.height(20.dp).width(itemSize)
+ }
+ )
+ }
+ }
+ }
+
+ state.scrollBy(itemSize)
+
+ rule.onNodeWithTag(LazyListTag)
+ .assertHeightIsEqualTo(30.dp)
+ }
+
+ private fun LazyListState.scrollBy(offset: Dp) {
+ runBlocking {
+ smoothScrollBy(with(rule.density) { offset.toIntPx().toFloat() }, snap())
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyScrollTest.kt
index 8f60ce8c..d216574 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyScrollTest.kt
@@ -90,12 +90,16 @@
@Composable
private fun TestContent() {
if (vertical) {
- LazyColumnFor(items, Modifier.preferredHeight(300.dp), state) {
- ItemContent()
+ LazyColumn(Modifier.preferredHeight(300.dp), state) {
+ items(items) {
+ ItemContent()
+ }
}
} else {
- LazyRowFor(items, Modifier.preferredWidth(300.dp), state) {
- ItemContent()
+ LazyRow(Modifier.preferredWidth(300.dp), state) {
+ items(items) {
+ ItemContent()
+ }
}
}
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
index 441aa85..6b681db 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
@@ -17,7 +17,6 @@
package androidx.compose.foundation.text
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.isFocused
import androidx.compose.ui.focusObserver
import androidx.compose.ui.platform.testTag
@@ -48,8 +47,7 @@
@OptIn(
ExperimentalTextApi::class,
- InternalTextApi::class,
- ExperimentalFocus::class
+ InternalTextApi::class
)
@LargeTest
@RunWith(AndroidJUnit4::class)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
new file mode 100644
index 0000000..3acb687
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import android.os.Build
+import android.view.View
+import android.view.WindowInsets
+import android.view.WindowInsetsAnimation
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.layout.Column
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focusRequester
+import androidx.compose.ui.platform.AmbientFocusManager
+import androidx.compose.ui.platform.AmbientView
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.text.InternalTextApi
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(InternalTextApi::class)
+class CoreTextFieldSoftKeyboardTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun keyboardShownOnInitialClick() {
+ // Arrange.
+ lateinit var view: View
+ rule.setContent {
+ view = AmbientView.current
+ CoreTextField(
+ value = TextFieldValue("Hello"),
+ onValueChange = {},
+ modifier = Modifier.testTag("TextField1")
+ )
+ }
+ view.ensureKeyboardIsHidden()
+
+ // Act.
+ val isSoftKeyboardShown = view.runAndWaitUntil({ view.isSoftwareKeyboardShown() }) {
+ rule.onNodeWithTag("TextField1").performClick()
+ }
+
+ // Assert.
+ assertThat(isSoftKeyboardShown).isTrue()
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun keyboardShownOnInitialFocus() {
+ // Arrange.
+ val focusRequester = FocusRequester()
+ lateinit var view: View
+ rule.setContent {
+ view = AmbientView.current
+ CoreTextField(
+ value = TextFieldValue("Hello"),
+ onValueChange = {},
+ modifier = Modifier.focusRequester(focusRequester)
+ )
+ }
+ view.ensureKeyboardIsHidden()
+
+ // Act.
+ val isSoftKeyboardShown = view.runAndWaitUntil({ view.isSoftwareKeyboardShown() }) {
+ rule.runOnIdle { focusRequester.requestFocus() }
+ }
+
+ // Assert.
+ assertThat(isSoftKeyboardShown).isTrue()
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun keyboardHiddenWhenFocusIsLost() {
+ // Arrange.
+ lateinit var focusManager: FocusManager
+ lateinit var view: View
+ val focusRequester = FocusRequester()
+ rule.setContent {
+ view = AmbientView.current
+ focusManager = AmbientFocusManager.current
+ CoreTextField(
+ value = TextFieldValue("Hello"),
+ onValueChange = {},
+ modifier = Modifier.focusRequester(focusRequester)
+ )
+ }
+ view.ensureKeyboardIsHidden()
+ // Request focus and wait for keyboard.
+ view.runAndWaitUntil({ view.isSoftwareKeyboardShown() }) {
+ rule.runOnIdle { focusRequester.requestFocus() }
+ }
+
+ // Act.
+ val isSoftKeyboardHidden = view.runAndWaitUntil({ !view.isSoftwareKeyboardShown() }) {
+ rule.runOnIdle { focusManager.clearFocus() }
+ }
+
+ // Assert.
+ assertThat(isSoftKeyboardHidden).isTrue()
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun keyboardShownAfterDismissingKeyboardAndClickingAgain() {
+ // Arrange.
+ lateinit var view: View
+ rule.setContent {
+ view = AmbientView.current
+ CoreTextField(
+ value = TextFieldValue("Hello"),
+ onValueChange = {},
+ modifier = Modifier.testTag("TextField1")
+ )
+ }
+ view.ensureKeyboardIsHidden()
+ view.runAndWaitUntil({ view.isSoftwareKeyboardShown() }) {
+ rule.onNodeWithTag("TextField1").performClick()
+ }
+ view.runAndWaitUntil({ !view.isSoftwareKeyboardShown() }) {
+ rule.runOnIdle { view.hideKeyboard() }
+ }
+
+ // Act.
+ val isSoftKeyboardVisible = view.runAndWaitUntil({ view.isSoftwareKeyboardShown() }) {
+ rule.onNodeWithTag("TextField1").performClick()
+ }
+
+ // Assert.
+ assertThat(isSoftKeyboardVisible).isTrue()
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun keyboardStaysVisibleWhenMovingFromOneTextFieldToAnother() {
+ // Arrange.
+ val focusRequester1 = FocusRequester()
+ val focusRequester2 = FocusRequester()
+ lateinit var view: View
+ rule.setContent {
+ view = AmbientView.current
+ Column {
+ CoreTextField(
+ value = TextFieldValue("Hello"),
+ onValueChange = {},
+ modifier = Modifier.focusRequester(focusRequester1)
+ )
+ CoreTextField(
+ value = TextFieldValue("Hello"),
+ onValueChange = {},
+ modifier = Modifier.focusRequester(focusRequester2)
+ )
+ }
+ }
+ view.ensureKeyboardIsHidden()
+ view.runAndWaitUntil({ view.isSoftwareKeyboardShown() }) {
+ rule.runOnIdle { focusRequester1.requestFocus() }
+ }
+
+ // Act.
+ val wasKeyboardHidden = view.runAndWaitUntil({ !view.isSoftwareKeyboardShown() }) {
+ rule.runOnIdle { focusRequester2.requestFocus() }
+ }
+
+ // Assert.
+ assertThat(wasKeyboardHidden).isFalse()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.R)
+ private fun View.runAndWaitUntil(condition: () -> Boolean, block: () -> Unit): Boolean {
+ val latch = CountDownLatch(1)
+ rule.runOnIdle {
+ rootView.setWindowInsetsAnimationCallback(
+ InsetAnimationCallback {
+ if (condition()) { latch.countDown() }
+ }
+ )
+ }
+ rule.waitForIdle()
+ block()
+ rule.waitForIdle()
+ return latch.await(15L, TimeUnit.SECONDS)
+ }
+
+ // We experienced some flakiness in tests if the keyboard was visible at the start of the test.
+ // This function makes sure the keyboard is hidden at the start of every test.
+ @RequiresApi(Build.VERSION_CODES.R)
+ private fun View.ensureKeyboardIsHidden() {
+ rule.waitForIdle()
+ if (isSoftwareKeyboardShown()) {
+ runAndWaitUntil({ !isSoftwareKeyboardShown() }) {
+ rule.runOnIdle { hideKeyboard() }
+ }
+ }
+ rule.waitForIdle()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.R)
+ private class InsetAnimationCallback(val block: () -> Unit) :
+ WindowInsetsAnimation.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
+
+ override fun onProgress(
+ insets: WindowInsets,
+ runningAnimations: MutableList<WindowInsetsAnimation>
+ ) = insets
+
+ override fun onEnd(animation: WindowInsetsAnimation) {
+ block()
+ super.onEnd(animation)
+ }
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.R)
+private fun View.isSoftwareKeyboardShown(): Boolean {
+ checkNotNull(rootWindowInsets)
+ return rootWindowInsets.isVisible(WindowInsets.Type.ime())
+}
+
+@RequiresApi(Build.VERSION_CODES.R)
+private fun View.hideKeyboard() {
+ windowInsetsController?.hide(WindowInsets.Type.ime())
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextFieldInteractionsTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextFieldInteractionsTest.kt
index 8050c7c..7d2d992 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextFieldInteractionsTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextFieldInteractionsTest.kt
@@ -23,7 +23,6 @@
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focusRequester
import androidx.compose.ui.geometry.Offset
@@ -49,7 +48,7 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
-@OptIn(InternalTextApi::class, ExperimentalFocus::class)
+@OptIn(InternalTextApi::class)
class TextFieldInteractionsTest {
@get:Rule
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
index 036f2c4..c338a45 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
@@ -25,6 +25,7 @@
import androidx.compose.ui.selection.Selection
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.InternalTextApi
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextDelegate
@@ -60,7 +61,10 @@
style = FontStyle.Normal
)
-@OptIn(InternalTextApi::class)
+@OptIn(
+ InternalTextApi::class,
+ ExperimentalTextApi::class
+)
@RunWith(AndroidJUnit4::class)
@MediumTest
class MultiWidgetSelectionDelegateTest {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
index 1570a3a..119d458 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
@@ -17,7 +17,7 @@
package androidx.compose.foundation.text.selection
import android.view.View
-import androidx.compose.ui.node.Owner
+import androidx.compose.ui.platform.ViewRootForTest
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.window.isPopupLayout
import androidx.test.espresso.Espresso
@@ -34,7 +34,7 @@
) {
// Make sure that current measurement/drawing is finished
runOnIdle { }
- Espresso.onView(CoreMatchers.instanceOf(Owner::class.java))
+ Espresso.onView(CoreMatchers.instanceOf(ViewRootForTest::class.java))
.inRoot(DoubleSelectionHandleMatcher(index))
.check(ViewAssertions.matches(viewMatcher))
}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/animation/AndroidFlingCalculator.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/animation/AndroidFlingCalculator.kt
index bb7cbcf..6bb3c64 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/animation/AndroidFlingCalculator.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/animation/AndroidFlingCalculator.kt
@@ -123,7 +123,7 @@
fun velocity(time: Long): Float {
val splinePos = if (duration > 0) time / duration.toFloat() else 1f
return AndroidFlingSpline.flingPosition(splinePos).velocityCoefficient *
- distance / duration * 1000.0f
+ sign(initialVelocity) * distance / duration * 1000.0f
}
}
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
index 627923d..8453061 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
@@ -25,7 +25,6 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.focus
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.isFocused
import androidx.compose.ui.focusObserver
import androidx.compose.ui.platform.debugInspectorInfo
@@ -43,7 +42,6 @@
* @param interactionState [InteractionState] that will be updated to contain [Interaction.Focused]
* when this focusable is focused
*/
-@OptIn(ExperimentalFocus::class)
fun Modifier.focusable(
enabled: Boolean = true,
interactionState: InteractionState? = null,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ProgressSemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ProgressSemantics.kt
index b5b0c00..409e25d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ProgressSemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ProgressSemantics.kt
@@ -19,8 +19,8 @@
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.AccessibilityRangeInfo
-import androidx.compose.ui.semantics.accessibilityValue
-import androidx.compose.ui.semantics.accessibilityValueRange
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.semantics.stateDescriptionRange
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.util.annotation.FloatRange
import androidx.compose.ui.util.format
@@ -52,8 +52,8 @@
}
return semantics {
- accessibilityValue = Strings.TemplatePercent.format(percent)
- accessibilityValueRange = AccessibilityRangeInfo(progress, 0f..1f)
+ stateDescription = Strings.TemplatePercent.format(percent)
+ stateDescriptionRange = AccessibilityRangeInfo(progress, 0f..1f)
}
}
@@ -69,5 +69,5 @@
*/
@Stable
fun Modifier.progressSemantics(): Modifier {
- return semantics { accessibilityValue = Strings.InProgress }
+ return semantics { stateDescription = Strings.InProgress }
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
index c30a7c6..aee281c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
@@ -478,7 +478,7 @@
if (isVertical) {
check(maxHeight != Constraints.Infinity) {
"Nesting scrollable in the same direction layouts like ScrollableContainer and " +
- "LazyColumnFor is not allowed. If you want to add a header before the list of" +
+ "LazyColumn is not allowed. If you want to add a header before the list of" +
" items please take a look on LazyColumn component which has a DSL api which" +
" allows to first add a header via item() function and then the list of " +
"items via items()."
@@ -486,7 +486,7 @@
} else {
check(maxWidth != Constraints.Infinity) {
"Nesting scrollable in the same direction layouts like ScrollableRow and " +
- "LazyRowFor is not allowed. If you want to add a header before the list of " +
+ "LazyRow is not allowed. If you want to add a header before the list of " +
"items please take a look on LazyRow component which has a DSL api which " +
"allows to first add a fixed element via item() function and then the " +
"list of items via items()."
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 4f6652d..1477c75 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
@@ -17,7 +17,6 @@
package androidx.compose.foundation.gestures
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.ExperimentalPointerInput
import androidx.compose.ui.input.pointer.HandlePointerInputScope
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
@@ -56,7 +55,6 @@
* @see awaitHorizontalTouchSlopOrCancellation
* @see awaitVerticalTouchSlopOrCancellation
*/
-@ExperimentalPointerInput
suspend fun HandlePointerInputScope.awaitTouchSlopOrCancellation(
pointerId: PointerId,
onTouchSlopReached: (change: PointerInputChange, overSlop: Offset) -> Unit
@@ -124,7 +122,6 @@
* @see horizontalDrag
* @see verticalDrag
*/
-@ExperimentalPointerInput
suspend fun HandlePointerInputScope.drag(
pointerId: PointerId,
onDrag: (PointerInputChange) -> Unit
@@ -159,7 +156,6 @@
* @see awaitHorizontalDragOrCancellation
* @see drag
*/
-@ExperimentalPointerInput
suspend fun HandlePointerInputScope.awaitDragOrCancellation(
pointerId: PointerId,
): PointerInputChange? {
@@ -184,7 +180,6 @@
* @see detectVerticalDragGestures
* @see detectHorizontalDragGestures
*/
-@ExperimentalPointerInput
suspend fun PointerInputScope.detectDragGestures(
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
@@ -235,7 +230,6 @@
* @see awaitHorizontalTouchSlopOrCancellation
* @see awaitTouchSlopOrCancellation
*/
-@ExperimentalPointerInput
suspend fun HandlePointerInputScope.awaitVerticalTouchSlopOrCancellation(
pointerId: PointerId,
onTouchSlopReached: (change: PointerInputChange, overSlop: Float) -> Unit
@@ -265,7 +259,6 @@
* @see horizontalDrag
* @see drag
*/
-@ExperimentalPointerInput
suspend fun HandlePointerInputScope.verticalDrag(
pointerId: PointerId,
onDrag: (PointerInputChange) -> Unit
@@ -293,7 +286,6 @@
* @see awaitDragOrCancellation
* @see verticalDrag
*/
-@ExperimentalPointerInput
suspend fun HandlePointerInputScope.awaitVerticalDragOrCancellation(
pointerId: PointerId,
): PointerInputChange? {
@@ -322,7 +314,6 @@
* @see detectDragGestures
* @see detectHorizontalDragGestures
*/
-@ExperimentalPointerInput
suspend fun PointerInputScope.detectVerticalDragGestures(
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
@@ -369,7 +360,6 @@
* @see awaitVerticalTouchSlopOrCancellation
* @see awaitTouchSlopOrCancellation
*/
-@ExperimentalPointerInput
suspend fun HandlePointerInputScope.awaitHorizontalTouchSlopOrCancellation(
pointerId: PointerId,
onTouchSlopReached: (change: PointerInputChange, overSlop: Float) -> Unit
@@ -396,7 +386,6 @@
* @see verticalDrag
* @see drag
*/
-@ExperimentalPointerInput
suspend fun HandlePointerInputScope.horizontalDrag(
pointerId: PointerId,
onDrag: (PointerInputChange) -> Unit
@@ -424,7 +413,6 @@
* @see awaitVerticalDragOrCancellation
* @see awaitDragOrCancellation
*/
-@ExperimentalPointerInput
suspend fun HandlePointerInputScope.awaitHorizontalDragOrCancellation(
pointerId: PointerId,
): PointerInputChange? {
@@ -453,7 +441,6 @@
* @see detectVerticalDragGestures
* @see detectDragGestures
*/
-@ExperimentalPointerInput
suspend fun PointerInputScope.detectHorizontalDragGestures(
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
@@ -488,7 +475,6 @@
* @return `true` when the gesture ended with all pointers up and `false` when the gesture
* was canceled.
*/
-@ExperimentalPointerInput
private suspend inline fun HandlePointerInputScope.drag(
pointerId: PointerId,
onDrag: (PointerInputChange) -> Unit,
@@ -522,7 +508,6 @@
* returned. When a drag is detected, that [PointerInputChange] is returned. A drag is
* only detected when [hasDragged] returns `true`.
*/
-@ExperimentalPointerInput
private suspend inline fun HandlePointerInputScope.awaitDragOrUp(
pointerId: PointerId,
hasDragged: (PointerInputChange) -> Boolean
@@ -568,7 +553,6 @@
* `null` if all pointers are raised or the position change was consumed by another gesture
* detector.
*/
-@ExperimentalPointerInput
private suspend inline fun HandlePointerInputScope.awaitTouchSlopOrCancellation(
pointerId: PointerId,
onTouchSlopReached: (PointerInputChange, Float) -> Unit,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ForEachGesture.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ForEachGesture.kt
index d3cd6e1..d545ee0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ForEachGesture.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ForEachGesture.kt
@@ -15,7 +15,6 @@
*/
package androidx.compose.foundation.gestures
-import androidx.compose.ui.gesture.ExperimentalPointerInput
import androidx.compose.ui.input.pointer.HandlePointerInputScope
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerInputScope
@@ -37,7 +36,6 @@
* exits if [isActive] is `false`.
*/
@OptIn(InternalCoroutinesApi::class, ExperimentalStdlibApi::class)
-@ExperimentalPointerInput
suspend fun PointerInputScope.forEachGesture(block: suspend PointerInputScope.() -> Unit) {
while (isActive) {
try {
@@ -59,14 +57,12 @@
* Returns `true` if the current state of the pointer events has all pointers up and `false`
* if any of the pointers are down.
*/
-@ExperimentalPointerInput
internal fun HandlePointerInputScope.allPointersUp(): Boolean =
!currentEvent.changes.fastAny { it.current.down }
/**
* Waits for all pointers to be up before returning.
*/
-@ExperimentalPointerInput
internal suspend fun PointerInputScope.awaitAllPointersUp() {
handlePointerInput { awaitAllPointersUp() }
}
@@ -74,7 +70,6 @@
/**
* Waits for all pointers to be up before returning.
*/
-@ExperimentalPointerInput
internal suspend fun HandlePointerInputScope.awaitAllPointersUp() {
if (!allPointersUp()) {
do {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MultitouchGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MultitouchGestureDetector.kt
index e7188c3..ccf797d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MultitouchGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/MultitouchGestureDetector.kt
@@ -17,7 +17,6 @@
package androidx.compose.foundation.gestures
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.ExperimentalPointerInput
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerInputScope
@@ -48,7 +47,6 @@
* Example Usage:
* @sample androidx.compose.foundation.samples.DetectMultitouchGestures
*/
-@ExperimentalPointerInput
suspend fun PointerInputScope.detectMultitouchGestures(
panZoomLock: Boolean = false,
onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index 830e5bc..6949e0b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -35,17 +35,25 @@
import androidx.compose.runtime.AtomicReference
import androidx.compose.runtime.Composable
import androidx.compose.runtime.dispatch.withFrameMillis
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.onDispose
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.gesture.Direction
import androidx.compose.ui.gesture.ScrollCallback
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollSource
+import androidx.compose.ui.gesture.nestedscroll.nestedScroll
import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
import androidx.compose.ui.platform.AmbientAnimationClock
import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.minus
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.sync.Mutex
@@ -258,24 +266,85 @@
}
private val animatedFloat =
- DeltaAnimatedFloat(0f, clocksProxy, consumeScrollDelta)
+ DeltaAnimatedFloat(0f, clocksProxy) {
+ dispatchScroll(it.reverseIfNeeded(), NestedScrollSource.Fling)
+ }
- /**
- * current position for scrollable
- */
- internal var value: Float
- get() = animatedFloat.value
- set(value) = animatedFloat.snapTo(value)
+ private var orientation by mutableStateOf(Orientation.Vertical)
+ private var reverseDirection by mutableStateOf(false)
- internal fun fling(velocity: Float, onScrollEnd: (Float) -> Unit) {
+ // this is not good, should be gone when we have sync (suspend) animation and scroll
+ internal fun update(orientation: Orientation, reverseDirection: Boolean) {
+ this.orientation = orientation
+ this.reverseDirection = reverseDirection
+ }
+
+ internal val nestedScrollDispatcher = NestedScrollDispatcher()
+
+ internal val nestedScrollConnection = object : NestedScrollConnection {
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset = performDeltaConsumption(available)
+
+ override fun onPostFling(
+ consumed: Velocity,
+ available: Velocity,
+ onFinished: (Velocity) -> Unit
+ ) {
+ performFlingInternal(available.pixelsPerSecond) { leftAfterUs ->
+ onFinished.invoke(available - Velocity(leftAfterUs))
+ }
+ }
+ }
+
+ internal fun dispatchScroll(scrollDelta: Float, source: NestedScrollSource) {
+ val scrollOffset = scrollDelta.toOffset()
+ val preConsumedByParent = nestedScrollDispatcher.dispatchPreScroll(scrollOffset, source)
+
+ val scrollAvailable = scrollOffset - preConsumedByParent
+ val consumed = performDeltaConsumption(scrollAvailable)
+ val leftForParent = scrollAvailable - consumed
+ nestedScrollDispatcher.dispatchPostScroll(consumed, leftForParent, source)
+ }
+
+ private fun performDeltaConsumption(delta: Offset): Offset {
+ // reverse once for users if needed and then back to the original axis system
+ return consumeScrollDelta(delta.toFloat().reverseIfNeeded()).reverseIfNeeded().toOffset()
+ }
+
+ internal fun dispatchFling(velocity: Float, onScrollEnd: (Float) -> Unit) {
+ val consumedByParent =
+ nestedScrollDispatcher.dispatchPreFling(Velocity(velocity.toOffset()))
+ val available = velocity.toOffset() - consumedByParent.pixelsPerSecond
+ performFlingInternal(available) { velocityLeft ->
+ // when notifying users code -- reverse if needed to obey their setting
+ onScrollEnd(velocityLeft.toFloat().reverseIfNeeded())
+ nestedScrollDispatcher.dispatchPostFling(
+ Velocity(available - velocityLeft),
+ Velocity(velocityLeft)
+ )
+ }
+ }
+
+ private fun performFlingInternal(velocity: Offset, onScrollEnd: (Offset) -> Unit) {
animatedFloat.fling(
config = flingConfig,
- startVelocity = velocity,
+ startVelocity = velocity.toFloat().reverseIfNeeded(),
onAnimationEnd = { _, _, velocityLeft ->
- onScrollEnd(velocityLeft)
+ onScrollEnd(velocityLeft.reverseIfNeeded().toOffset())
}
)
}
+
+ private fun Float.toOffset(): Offset =
+ if (orientation == Orientation.Horizontal) Offset(this, 0f) else Offset(0f, this)
+
+ private fun Offset.toFloat(): Float =
+ if (orientation == Orientation.Horizontal) this.x else this.y
+
+ private fun Float.reverseIfNeeded(): Float = if (reverseDirection) this * -1 else this
}
/**
@@ -315,6 +384,7 @@
onScrollStopped: (velocity: Float) -> Unit = {}
): Modifier = composed(
factory = {
+ controller.update(orientation, reverseDirection)
onDispose {
controller.stopAnimation()
controller.interactionState?.removeInteraction(Interaction.Dragged)
@@ -333,10 +403,9 @@
override fun onScroll(scrollDistance: Float): Float {
if (!enabled) return 0f
controller.stopFlingAnimation()
- val toConsume = if (reverseDirection) scrollDistance * -1 else scrollDistance
- val consumed = controller.consumeScrollDelta(toConsume)
- controller.value = controller.value + consumed
- return if (reverseDirection) consumed * -1 else consumed
+ controller.dispatchScroll(scrollDistance, NestedScrollSource.Drag)
+ // consume everything since we handle nested scrolling separately
+ return scrollDistance
}
override fun onCancel() {
@@ -349,8 +418,8 @@
override fun onStop(velocity: Float) {
controller.interactionState?.removeInteraction(Interaction.Dragged)
if (enabled) {
- controller.fling(
- velocity = if (reverseDirection) velocity * -1 else velocity,
+ controller.dispatchFling(
+ velocity = velocity,
onScrollEnd = onScrollStopped
)
}
@@ -365,7 +434,7 @@
).mouseScrollable(
scrollCallback,
orientation
- )
+ ).nestedScroll(controller.nestedScrollConnection, controller.nestedScrollDispatcher)
},
inspectorInfo = debugInspectorInfo {
name = "scrollable"
@@ -398,7 +467,7 @@
private class DeltaAnimatedFloat(
initial: Float,
clock: AnimationClockObservable,
- private val onDelta: (Float) -> Float
+ private val onDelta: (Float) -> Unit
) : AnimatedFloat(clock, Spring.DefaultDisplacementThreshold) {
override var value = initial
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
index d9f141b..ca356ef 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
@@ -17,7 +17,6 @@
package androidx.compose.foundation.gestures
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.ExperimentalPointerInput
import androidx.compose.ui.input.pointer.HandlePointerInputScope
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
@@ -86,7 +85,6 @@
* the gestures are considered canceled. [onDoubleTap], [onLongPress], and [onTap] will not be
* called after a gesture has been canceled.
*/
-@ExperimentalPointerInput
suspend fun PointerInputScope.detectTapGestures(
onDoubleTap: (() -> Unit)? = null,
onLongPress: (() -> Unit)? = null,
@@ -186,7 +184,6 @@
* Reads events until the first down is received. If [requireUnconsumed] is `true` and the first
* down is consumed in the [PointerEventPass.Main] pass, that gesture is ignored.
*/
-@ExperimentalPointerInput
suspend fun HandlePointerInputScope.awaitFirstDown(
requireUnconsumed: Boolean = true
): PointerInputChange {
@@ -208,7 +205,6 @@
* pass. If the gesture was not canceled, the final up change is returned or `null` if the
* event was canceled.
*/
-@ExperimentalPointerInput
suspend fun HandlePointerInputScope.waitForUpOrCancellation(): PointerInputChange? {
while (true) {
val event = awaitPointerEvent(PointerEventPass.Main)
@@ -233,7 +229,6 @@
/**
* Consumes all event changes in the [PointerEventPass.Initial] until all pointers are up.
*/
-@ExperimentalPointerInput
private suspend fun PointerInputScope.consumeAllEventsUntilUp() {
handlePointerInput {
if (!allPointersUp()) {
@@ -251,7 +246,6 @@
* not detected within [ViewConfiguration.doubleTapTimeout] of [upTime], `null` is returned.
* Otherwise, the down event is returned.
*/
-@ExperimentalPointerInput
private suspend fun PointerInputScope.detectSecondTapDown(
upTime: Uptime
): PointerInputChange? {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
index e3ebd07..9da735e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
@@ -16,6 +16,8 @@
package androidx.compose.foundation.lazy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.InternalLayoutApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -170,17 +172,28 @@
* @param modifier the modifier to apply to this layout
* @param state the state object to be used to control or observe the list's state
* @param contentPadding a padding around the whole content. This will add padding for the
- * content after it has been clipped, which is not possible via [modifier] param. Note that it is
- * **not** a padding applied for each item's content
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first item or after the last one. If you want to add a spacing
+ * between each item use [horizontalArrangement].
+ * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
+ * composed from the end to the start and [LazyListState.firstVisibleItemIndex] == 0 will mean
+ * the first item is located at the end.
+ * @param horizontalArrangement The horizontal arrangement of the layout's children. This allows
+ * to add a spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size.
* @param verticalAlignment the vertical alignment applied to the items
* @param content a block which describes the content. Inside this block you can use methods like
* [LazyListScope.item] to add a single item or [LazyListScope.items] to add a list of items.
*/
+@OptIn(InternalLayoutApi::class)
@Composable
fun LazyRow(
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ horizontalArrangement: Arrangement.Horizontal =
+ if (!reverseLayout) Arrangement.Start else Arrangement.End,
verticalAlignment: Alignment.Vertical = Alignment.Top,
content: LazyListScope.() -> Unit
) {
@@ -193,7 +206,9 @@
state = state,
contentPadding = contentPadding,
verticalAlignment = verticalAlignment,
- isVertical = false
+ horizontalArrangement = horizontalArrangement,
+ isVertical = false,
+ reverseLayout = reverseLayout
) { index ->
scope.contentFor(index, this)
}
@@ -207,20 +222,31 @@
*
* @sample androidx.compose.foundation.samples.LazyColumnSample
*
- * @param modifier the modifier to apply to this layout
- * @param state the state object to be used to control or observe the list's state
- * @param contentPadding a padding around the whole content. This will add padding for the
- * content after it has been clipped, which is not possible via [modifier] param. Note that it is
- * **not** a padding applied for each item's content
- * @param horizontalAlignment the horizontal alignment applied to the items
+ * @param modifier the modifier to apply to this layout.
+ * @param state the state object to be used to control or observe the list's state.
+ * @param contentPadding a padding around the whole content. This will add padding for the.
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first item or after the last one. If you want to add a spacing
+ * between each item use [verticalArrangement].
+ * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
+ * composed from the bottom to the top and [LazyListState.firstVisibleItemIndex] == 0 will mean
+ * we scrolled to the bottom.
+ * @param verticalArrangement The vertical arrangement of the layout's children. This allows
+ * to add a spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size.
+ * @param horizontalAlignment the horizontal alignment applied to the items.
* @param content a block which describes the content. Inside this block you can use methods like
* [LazyListScope.item] to add a single item or [LazyListScope.items] to add a list of items.
*/
+@OptIn(InternalLayoutApi::class)
@Composable
fun LazyColumn(
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ verticalArrangement: Arrangement.Vertical =
+ if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: LazyListScope.() -> Unit
) {
@@ -233,7 +259,9 @@
state = state,
contentPadding = contentPadding,
horizontalAlignment = horizontalAlignment,
- isVertical = true
+ verticalArrangement = verticalArrangement,
+ isVertical = true,
+ reverseLayout = reverseLayout
) { index ->
scope.contentFor(index, this)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyFor.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyFor.kt
index 2031273..89b9186 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyFor.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyFor.kt
@@ -16,6 +16,8 @@
package androidx.compose.foundation.lazy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.InternalLayoutApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.runtime.Composable
@@ -29,14 +31,19 @@
* See [LazyColumnForIndexed] if you need to have both item and index params in [itemContent].
* See [LazyRowFor] if you are looking for a horizontally scrolling version.
*
- * @sample androidx.compose.foundation.samples.LazyColumnForSample
- *
* @param items the backing list of data to display
* @param modifier the modifier to apply to this layout
* @param state the state object to be used to control or observe the list's state
- * @param contentPadding a padding around the whole content. This will add padding for the
- * content after it has been clipped, which is not possible via [modifier] param. Note that it is
- * **not** a padding applied for each item's content
+ * @param contentPadding a padding around the whole content. This will add padding for the.
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first item or after the last one. If you want to add a spacing
+ * between each item use [verticalArrangement].
+ * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
+ * composed from the bottom to the top and [LazyListState.firstVisibleItemIndex] == 0 will mean
+ * we scrolled to the bottom.
+ * @param verticalArrangement The vertical arrangement of the layout's children. This allows
+ * to add a spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size.
* @param horizontalAlignment the horizontal alignment applied to the items
* @param itemContent emits the UI for an item from [items] list. May emit any number of components,
* which will be stacked vertically. Note that [LazyColumnFor] can start scrolling incorrectly
@@ -44,12 +51,24 @@
* content asynchronously please reserve some space for the item, for example using [Spacer].
* Use [LazyColumnForIndexed] if you need to have both index and item params.
*/
+@OptIn(InternalLayoutApi::class)
@Composable
+@Deprecated(
+ "Use LazyColumn instead",
+ ReplaceWith(
+ "LazyColumn(modifier, state, contentPadding, horizontalAlignment = " +
+ "horizontalAlignment) { \n items(items, itemContent) \n }",
+ "androidx.compose.foundation.lazy.LazyColumn"
+ )
+)
fun <T> LazyColumnFor(
items: List<T>,
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ verticalArrangement: Arrangement.Vertical =
+ if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
itemContent: @Composable LazyItemScope.(T) -> Unit
) {
@@ -57,7 +76,9 @@
modifier = modifier,
state = state,
contentPadding = contentPadding,
- horizontalAlignment = horizontalAlignment
+ horizontalAlignment = horizontalAlignment,
+ verticalArrangement = verticalArrangement,
+ reverseLayout = reverseLayout
) {
items(items, itemContent)
}
@@ -71,14 +92,19 @@
*
* See [LazyRowForIndexed] if you are looking for a horizontally scrolling version.
*
- * @sample androidx.compose.foundation.samples.LazyColumnForIndexedSample
- *
* @param items the backing list of data to display
* @param modifier the modifier to apply to this layout
* @param state the state object to be used to control or observe the list's state
- * @param contentPadding a padding around the whole content. This will add padding for the
- * content after it has been clipped, which is not possible via [modifier] param. Note that it is
- * **not** a padding applied for each item's content
+ * @param contentPadding a padding around the whole content. This will add padding for the.
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first item or after the last one. If you want to add a spacing
+ * between each item use [verticalArrangement].
+ * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
+ * composed from the bottom to the top and [LazyListState.firstVisibleItemIndex] == 0 will mean
+ * we scrolled to the bottom.
+ * @param verticalArrangement The vertical arrangement of the layout's children. This allows
+ * to add a spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size.
* @param horizontalAlignment the horizontal alignment applied to the items
* @param itemContent emits the UI for an item from [items] list. It has two params: first one is
* an index in the [items] list, and the second one is the item at this index from [items] list.
@@ -87,12 +113,24 @@
* recompose with the real content, so even if you load the content asynchronously please reserve
* some space for the item, for example using [Spacer].
*/
+@OptIn(InternalLayoutApi::class)
@Composable
+@Deprecated(
+ "Use LazyColumn instead",
+ ReplaceWith(
+ "LazyColumn(modifier, state, contentPadding, horizontalAlignment = " +
+ "horizontalAlignment) { \n itemsIndexed(items, itemContent) \n }",
+ "androidx.compose.foundation.lazy.LazyColumn"
+ )
+)
fun <T> LazyColumnForIndexed(
items: List<T>,
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ verticalArrangement: Arrangement.Vertical =
+ if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
) {
@@ -100,7 +138,9 @@
modifier = modifier,
state = state,
contentPadding = contentPadding,
- horizontalAlignment = horizontalAlignment
+ horizontalAlignment = horizontalAlignment,
+ verticalArrangement = verticalArrangement,
+ reverseLayout = reverseLayout
) {
itemsIndexed(items, itemContent)
}
@@ -112,27 +152,44 @@
* See [LazyRowForIndexed] if you need to have both item and index params in [itemContent].
* See [LazyColumnFor] if you are looking for a vertically scrolling version.
*
- * @sample androidx.compose.foundation.samples.LazyRowForSample
- *
- * @param items the backing list of data to display
- * @param modifier the modifier to apply to this layout
- * @param state the state object to be used to control or observe the list's state
+ * @param items the backing list of data to display.
+ * @param modifier the modifier to apply to this layout.
+ * @param state the state object to be used to control or observe the list's state.
* @param contentPadding a padding around the whole content. This will add padding for the
- * content after it has been clipped, which is not possible via [modifier] param. Note that it is
- * **not** a padding applied for each item's content
- * @param verticalAlignment the vertical alignment applied to the items
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first item or after the last one. If you want to add a spacing
+ * between each item use [horizontalArrangement].
+ * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
+ * composed from the end to the start and [LazyListState.firstVisibleItemIndex] == 0 will mean
+ * the first item is located at the end.
+ * @param horizontalArrangement The horizontal arrangement of the layout's children. This allows
+ * to add a spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size.
+ * @param verticalAlignment the vertical alignment applied to the items.
* @param itemContent emits the UI for an item from [items] list. May emit any number of components,
* which will be stacked horizontally. Note that [LazyRowFor] can start scrolling incorrectly
* if you emit nothing and then lazily recompose with the real content, so even if you load the
* content asynchronously please reserve some space for the item, for example using [Spacer].
* Use [LazyRowForIndexed] if you need to have both index and item params.
*/
+@OptIn(InternalLayoutApi::class)
@Composable
+@Deprecated(
+ "Use LazyRow instead",
+ ReplaceWith(
+ "LazyRow(modifier, state, contentPadding, verticalAlignment = " +
+ "verticalAlignment) { \n items(items, itemContent) \n }",
+ "androidx.compose.foundation.lazy.LazyColumn"
+ )
+)
fun <T> LazyRowFor(
items: List<T>,
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ horizontalArrangement: Arrangement.Horizontal =
+ if (!reverseLayout) Arrangement.Start else Arrangement.End,
verticalAlignment: Alignment.Vertical = Alignment.Top,
itemContent: @Composable LazyItemScope.(T) -> Unit
) {
@@ -141,6 +198,8 @@
state = state,
contentPadding = contentPadding,
verticalAlignment = verticalAlignment,
+ horizontalArrangement = horizontalArrangement,
+ reverseLayout = reverseLayout
) {
items(items, itemContent)
}
@@ -153,15 +212,20 @@
*
* See [LazyColumnForIndexed] if you are looking for a vertically scrolling version.
*
- * @sample androidx.compose.foundation.samples.LazyRowForIndexedSample
- *
- * @param items the backing list of data to display
- * @param modifier the modifier to apply to this layout
- * @param state the state object to be used to control or observe the list's state
+ * @param items the backing list of data to display.
+ * @param modifier the modifier to apply to this layout.
+ * @param state the state object to be used to control or observe the list's state.
* @param contentPadding a padding around the whole content. This will add padding for the
- * content after it has been clipped, which is not possible via [modifier] param. Note that it is
- * **not** a padding applied for each item's content
- * @param verticalAlignment the vertical alignment applied to the items
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first item or after the last one. If you want to add a spacing
+ * between each item use [horizontalArrangement].
+ * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
+ * composed from the end to the start and [LazyListState.firstVisibleItemIndex] == 0 will mean
+ * the first item is located at the end.
+ * @param horizontalArrangement The horizontal arrangement of the layout's children. This allows
+ * to add a spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size.
+ * @param verticalAlignment the vertical alignment applied to the items.
* @param itemContent emits the UI for an item from [items] list. It has two params: first one is
* an index in the [items] list, and the second one is the item at this index from [items] list.
* May emit any number of components, which will be stacked horizontally. Note that
@@ -169,12 +233,24 @@
* recompose with the real content, so even if you load the content asynchronously please reserve
* some space for the item, for example using [Spacer].
*/
+@OptIn(InternalLayoutApi::class)
@Composable
+@Deprecated(
+ "Use LazyRow instead",
+ ReplaceWith(
+ "LazyRow(modifier, state, contentPadding, verticalAlignment = " +
+ "verticalAlignment) { \n itemsIndexed(items, itemContent) \n }",
+ "androidx.compose.foundation.lazy.LazyColumn"
+ )
+)
fun <T> LazyRowForIndexed(
items: List<T>,
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
+ reverseLayout: Boolean = false,
+ horizontalArrangement: Arrangement.Horizontal =
+ if (!reverseLayout) Arrangement.Start else Arrangement.End,
verticalAlignment: Alignment.Vertical = Alignment.Top,
itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
) {
@@ -183,6 +259,8 @@
state = state,
contentPadding = contentPadding,
verticalAlignment = verticalAlignment,
+ horizontalArrangement = horizontalArrangement,
+ reverseLayout = reverseLayout
) {
itemsIndexed(items, itemContent)
}
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 a8f98a8..2bcbed5 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
@@ -16,10 +16,12 @@
package androidx.compose.foundation.lazy
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Constraints
@@ -27,7 +29,7 @@
/**
* The DSL implementation of a lazy grid layout. It composes only visible rows of the grid.
- * This API is not stable, please consider using stable components like [LazyColumnFor] and [Row]
+ * This API is not stable, please consider using stable components like [LazyColumn] and [Row]
* to achieve the same result.
*
* @param columns a fixed number of columns of the grid
@@ -52,7 +54,10 @@
modifier = modifier,
state = state,
contentPadding = contentPadding,
- isVertical = true
+ isVertical = true,
+ horizontalAlignment = Alignment.Start,
+ verticalArrangement = Arrangement.Top,
+ reverseLayout = false
) { rowIndex ->
{
GridRow {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
index 10aaf67..69971f10 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
@@ -18,10 +18,14 @@
import androidx.compose.foundation.assertNotNestingScrollableContainers
import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.InternalLayoutApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
+import androidx.compose.runtime.savedinstancestate.ExperimentalRestorableStateHolder
+import androidx.compose.runtime.savedinstancestate.rememberRestorableStateHolder
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
@@ -29,25 +33,41 @@
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.AmbientLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.constrainHeight
-import androidx.compose.ui.unit.constrainWidth
-import androidx.compose.ui.util.fastForEach
+@OptIn(InternalLayoutApi::class)
@Composable
internal fun LazyList(
+ /** The total size of the list */
itemsCount: Int,
- modifier: Modifier = Modifier,
+ /** Modifier to be applied for the inner layout */
+ modifier: Modifier,
+ /** State controlling the scroll position */
state: LazyListState,
+ /** The inner padding to be added for the whole content(nor for each individual item) */
contentPadding: PaddingValues,
- horizontalAlignment: Alignment.Horizontal = Alignment.Start,
- verticalAlignment: Alignment.Vertical = Alignment.Top,
+ /** reverse the direction of scrolling and layout */
+ reverseLayout: Boolean,
+ /** The layout orientation of the list */
isVertical: Boolean,
- itemContentFactory: LazyItemScope.(Int) -> @Composable () -> Unit
+ /** The alignment to align items horizontally. Required when isVertical is true */
+ horizontalAlignment: Alignment.Horizontal? = null,
+ /** The vertical arrangement for items. Required when isVertical is true */
+ verticalArrangement: Arrangement.Vertical? = null,
+ /** The alignment to align items vertically. Required when isVertical is false */
+ verticalAlignment: Alignment.Vertical? = null,
+ /** The horizontal arrangement for items. Required when isVertical is false */
+ horizontalArrangement: Arrangement.Horizontal? = null,
+ /** The factory defining the content for an item on the given position in the list */
+ itemContent: LazyItemScope.(Int) -> @Composable () -> Unit
) {
- val reverseDirection = AmbientLayoutDirection.current == LayoutDirection.Rtl && !isVertical
+ val isRtl = AmbientLayoutDirection.current == LayoutDirection.Rtl
+ // reverse scroll by default, to have "natural" gesture that goes reversed to layout
+ // if rtl and horizontal, do not reverse to make it right-to-left
+ val reverseScrollDirection = if (!isVertical && isRtl) reverseLayout else !reverseLayout
- val cachingItemContentFactory = remember { CachingItemContentFactory(itemContentFactory) }
- cachingItemContentFactory.itemContentFactory = itemContentFactory
+ val restorableItemContent = wrapWithStateRestoration(itemContent)
+ val cachingItemContentFactory = remember { CachingItemContentFactory(restorableItemContent) }
+ cachingItemContentFactory.itemContentFactory = restorableItemContent
val startContentPadding = if (isVertical) contentPadding.top else contentPadding.start
val endContentPadding = if (isVertical) contentPadding.bottom else contentPadding.end
@@ -55,8 +75,7 @@
modifier
.scrollable(
orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
- // reverse scroll by default, to have "natural" gesture that goes reversed to layout
- reverseDirection = !reverseDirection,
+ reverseDirection = reverseScrollDirection,
controller = state.scrollableController
)
.clipToBounds()
@@ -71,21 +90,32 @@
val startContentPaddingPx = startContentPadding.toIntPx()
val endContentPaddingPx = endContentPadding.toIntPx()
val mainAxisMaxSize = (if (isVertical) constraints.maxHeight else constraints.maxWidth)
+ val spaceBetweenItemsDp = if (isVertical) {
+ requireNotNull(verticalArrangement).spacing
+ } else {
+ requireNotNull(horizontalArrangement).spacing
+ }
+ val spaceBetweenItems = spaceBetweenItemsDp.toIntPx()
val itemProvider = LazyMeasuredItemProvider(
constraints,
isVertical,
this,
cachingItemContentFactory
- ) { placeables ->
+ ) { index, placeables ->
+ // we add spaceBetweenItems as an extra spacing for all items apart from the last one so
+ // the lazy list measuring logic will take it into account.
+ val spacing = if (index.value == itemsCount - 1) 0 else spaceBetweenItems
LazyMeasuredItem(
+ index = index.value,
placeables = placeables,
isVertical = isVertical,
horizontalAlignment = horizontalAlignment,
verticalAlignment = verticalAlignment,
layoutDirection = layoutDirection,
startContentPadding = startContentPaddingPx,
- endContentPadding = endContentPaddingPx
+ endContentPadding = endContentPaddingPx,
+ spacing = spacing
)
}
@@ -102,18 +132,33 @@
state.applyMeasureResult(measureResult)
- val layoutWidth = constraints.constrainWidth(
- if (isVertical) measureResult.crossAxisSize else measureResult.mainAxisSize
+ layoutLazyList(
+ constraints,
+ isVertical,
+ verticalArrangement,
+ horizontalArrangement,
+ measureResult,
+ reverseLayout
)
- val layoutHeight = constraints.constrainHeight(
- if (isVertical) measureResult.mainAxisSize else measureResult.crossAxisSize
- )
- layout(layoutWidth, layoutHeight) {
- var currentMainAxis = measureResult.itemsScrollOffset
- measureResult.items.fastForEach {
- it.place(this, layoutWidth, layoutHeight, currentMainAxis)
- currentMainAxis += it.mainAxisSize
- }
+ }
+}
+
+/**
+ * Converts item content factory to another one which adds auto state restoration functionality.
+ */
+@OptIn(ExperimentalRestorableStateHolder::class)
+@Composable
+internal fun wrapWithStateRestoration(
+ itemContentFactory: LazyItemScope.(Int) -> @Composable () -> Unit
+): LazyItemScope.(Int) -> @Composable () -> Unit {
+ val restorableStateHolder = rememberRestorableStateHolder<Any>()
+ return remember(itemContentFactory) {
+ { index ->
+ val content = itemContentFactory(index)
+ // we just wrap our original lambda with the one which auto restores the state
+ // currently we use index in the list as a key for the restoration, but in the future
+ // we will use the user provided key
+ (@Composable { restorableStateHolder.RestorableStateProvider(index, content) })
}
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemInfo.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemInfo.kt
new file mode 100644
index 0000000..568f577
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemInfo.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+/**
+ * Contains useful information about an individual item in lazy lists like [LazyColumn] or [LazyRow].
+ *
+ * @see LazyListLayoutInfo
+ */
+interface LazyListItemInfo {
+ /**
+ * The index of the item in the list.
+ */
+ val index: Int
+
+ /**
+ * The main axis offset of the item. It is relative to the start of the lazy list container.
+ */
+ val offset: Int
+
+ /**
+ * The main axis size of the item. Note that if you emit multiple layouts in the composable
+ * slot for the item then this size will be calculated as the sum of their sizes.
+ */
+ val size: Int
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfo.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfo.kt
new file mode 100644
index 0000000..40acb47
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfo.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy
+
+/**
+ * Contains useful information about the currently displayed layout state of lazy lists like
+ * [LazyColumn] or [LazyRow]. For example you can get the list of currently displayed item.
+ *
+ * Use [LazyListState.layoutInfo] to retrieve this
+ */
+interface LazyListLayoutInfo {
+ /**
+ * The list of [LazyListItemInfo] representing all the currently visible items.
+ */
+ val visibleItemsInfo: List<LazyListItemInfo>
+
+ /**
+ * The start offset of the layout's viewport. You can think of it as a minimum offset which
+ * would be visible. Usually it is 0, but it can be negative if a content padding was applied
+ * as the content displayed in the content padding area is still visible.
+ *
+ * You can use it to understand what items from [visibleItemsInfo] are fully visible.
+ */
+ val viewportStartOffset: Int
+
+ /**
+ * The end offset of the layout's viewport. You can think of it as a maximum offset which
+ * would be visible. Usually it is a size of the lazy list container plus a content padding.
+ *
+ * You can use it to understand what items from [visibleItemsInfo] are fully visible.
+ */
+ val viewportEndOffset: Int
+
+ /**
+ * The total count of items passed to [LazyColumn] or [LazyRow].
+ */
+ val totalItemsCount: Int
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
index 41c2fcf..3e4670e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
@@ -16,12 +16,21 @@
package androidx.compose.foundation.lazy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.InternalLayoutApi
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.util.fastForEach
import kotlin.math.abs
import kotlin.math.roundToInt
import kotlin.math.sign
/**
- * Measures and positions currently visible items using [LazyMeasuredItemProvider].
+ * Measures and calculates the positions for the currently visible items. The result is produced
+ * as a [LazyListMeasureResult] which contains all the calculations.
*/
internal fun measureLazyList(
itemsCount: Int,
@@ -45,7 +54,10 @@
firstVisibleItemIndex = DataIndex(0),
firstVisibleItemScrollOffset = 0,
canScrollForward = false,
- consumedScroll = 0f
+ consumedScroll = 0f,
+ viewportStartOffset = -startContentPadding,
+ viewportEndOffset = endContentPadding,
+ totalItemsCount = 0
)
} else {
var currentFirstItemIndex = firstVisibleItemIndex
@@ -97,7 +109,7 @@
val measuredItem = itemProvider.getAndMeasure(previous)
visibleItems.add(0, measuredItem)
maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
- currentFirstItemScrollOffset += measuredItem.mainAxisSize
+ currentFirstItemScrollOffset += measuredItem.sizeWithSpacings
currentFirstItemIndex = previous
}
// if we were scrolled backward, but there were not enough items before. this means
@@ -120,12 +132,12 @@
var mainAxisUsed = -goingForwardInitialScrollOffset
while (mainAxisUsed <= maxMainAxis && index.value < itemsCount) {
val measuredItem = itemProvider.getAndMeasure(index)
- mainAxisUsed += measuredItem.mainAxisSize
+ mainAxisUsed += measuredItem.sizeWithSpacings
if (mainAxisUsed < minOffset) {
// this item is offscreen and will not be placed. advance firstVisibleItemIndex
currentFirstItemIndex = index + 1
- currentFirstItemScrollOffset -= measuredItem.mainAxisSize
+ currentFirstItemScrollOffset -= measuredItem.sizeWithSpacings
// but remember the corresponding placeables in case we will be forced to
// scroll back as there were not enough items to fill the viewport
if (notUsedButComposedItems == null) {
@@ -156,7 +168,7 @@
}
visibleItems.add(0, measuredItem)
maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
- currentFirstItemScrollOffset += measuredItem.mainAxisSize
+ currentFirstItemScrollOffset += measuredItem.sizeWithSpacings
currentFirstItemIndex = previous
}
scrollDelta += toScrollBack
@@ -189,7 +201,7 @@
currentFirstItemScrollOffset += startContentPadding
var startPaddingItems = 0
while (startPaddingItems < visibleItems.lastIndex) {
- val size = visibleItems[startPaddingItems].mainAxisSize
+ val size = visibleItems[startPaddingItems].sizeWithSpacings
if (size <= currentFirstItemScrollOffset) {
startPaddingItems++
currentFirstItemScrollOffset -= size
@@ -200,15 +212,77 @@
}
}
+ val mainAxisSize = mainAxisUsed + startContentPadding
+ val maximumVisibleOffset = minOf(mainAxisSize, mainAxisMaxSize) + endContentPadding
return LazyListMeasureResult(
- mainAxisSize = mainAxisUsed + startContentPadding,
+ mainAxisSize = mainAxisSize,
crossAxisSize = maxCrossAxis,
items = visibleItems,
itemsScrollOffset = firstItemOffset,
firstVisibleItemIndex = currentFirstItemIndex,
firstVisibleItemScrollOffset = currentFirstItemScrollOffset,
canScrollForward = mainAxisUsed > maxOffset,
- consumedScroll = consumedScroll
+ consumedScroll = consumedScroll,
+ viewportStartOffset = -startContentPadding,
+ viewportEndOffset = maximumVisibleOffset,
+ totalItemsCount = itemsCount
)
}
}
+
+/**
+ * Lays out [LazyMeasuredItem]s based on the [LazyListMeasureResult] and the passed arrangement.
+ */
+@OptIn(InternalLayoutApi::class)
+internal fun MeasureScope.layoutLazyList(
+ constraints: Constraints,
+ isVertical: Boolean,
+ verticalArrangement: Arrangement.Vertical?,
+ horizontalArrangement: Arrangement.Horizontal?,
+ measureResult: LazyListMeasureResult,
+ reverseLayout: Boolean
+): MeasureResult {
+ val layoutWidth = constraints.constrainWidth(
+ if (isVertical) measureResult.crossAxisSize else measureResult.mainAxisSize
+ )
+ val layoutHeight = constraints.constrainHeight(
+ if (isVertical) measureResult.mainAxisSize else measureResult.crossAxisSize
+ )
+ val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
+ val hasSpareSpace = measureResult.mainAxisSize < mainAxisLayoutSize
+ if (hasSpareSpace) {
+ check(measureResult.itemsScrollOffset == 0)
+ }
+ val density = this
+
+ return layout(layoutWidth, layoutHeight) {
+ var currentMainAxis = measureResult.itemsScrollOffset
+ if (hasSpareSpace) {
+ val items = if (reverseLayout) measureResult.items.reversed() else measureResult.items
+ val sizes = IntArray(items.size) { index ->
+ items[index].size
+ }
+ val positions = IntArray(items.size) { 0 }
+ if (isVertical) {
+ requireNotNull(verticalArrangement)
+ .arrange(mainAxisLayoutSize, sizes, density, positions)
+ } else {
+ requireNotNull(horizontalArrangement)
+ .arrange(mainAxisLayoutSize, sizes, layoutDirection, density, positions)
+ }
+ positions.forEachIndexed { index, position ->
+ items[index].place(this, layoutWidth, layoutHeight, position, reverseLayout)
+ }
+ } else {
+ measureResult.items.fastForEach {
+ val offset = if (reverseLayout) {
+ mainAxisLayoutSize - currentMainAxis - (it.size)
+ } else {
+ currentMainAxis
+ }
+ it.place(this, layoutWidth, layoutHeight, offset, reverseLayout)
+ currentMainAxis += it.sizeWithSpacings
+ }
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt
index f3a3fcd..02b13b3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt
@@ -35,5 +35,10 @@
/** True if there is some space available to continue scrolling in the forward direction.*/
val canScrollForward: Boolean,
/** The amount of scroll consumed during the measure pass.*/
- val consumedScroll: Float
-)
+ val consumedScroll: Float,
+ override val viewportStartOffset: Int,
+ override val viewportEndOffset: Int,
+ override val totalItemsCount: Int
+) : LazyListLayoutInfo {
+ override val visibleItemsInfo: List<LazyListItemInfo> get() = items
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
index 52250e1..53b32b2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
@@ -118,7 +118,7 @@
val firstVisibleItemScrollOffset: Int get() = scrollPosition.observableScrollOffset
/**
- * whether this [LazyListState] is currently scrolling via [scroll] or via an
+ * Whether this [LazyListState] is currently scrolling via [scroll] or via an
* animation/fling.
*
* Note: **all** scrolls initiated via [scroll] are considered to be animations, regardless of
@@ -127,6 +127,15 @@
val isAnimationRunning
get() = scrollableController.isAnimationRunning
+ /** Backing state for [layoutInfo] */
+ private val layoutInfoState = mutableStateOf<LazyListLayoutInfo>(EmptyLazyListLayoutInfo)
+
+ /**
+ * The object of [LazyListLayoutInfo] calculated during the last layout pass. For example,
+ * you can use it to calculate what items are currently visible.
+ */
+ val layoutInfo: LazyListLayoutInfo get() = layoutInfoState.value
+
/**
* The amount of scroll to be consumed in the next layout pass. Scrolling forward is negative
* - that is, it is the amount that the items are offset in y
@@ -282,6 +291,7 @@
canScrollForward = measureResult.canScrollForward
)
scrollToBeConsumed -= measureResult.consumedScroll
+ layoutInfoState.value = measureResult
numMeasurePasses++
}
@@ -353,3 +363,10 @@
this.canScrollForward = canScrollForward
}
}
+
+private object EmptyLazyListLayoutInfo : LazyListLayoutInfo {
+ override val visibleItemsInfo = emptyList<LazyListItemInfo>()
+ override val viewportStartOffset = 0
+ override val viewportEndOffset = 0
+ override val totalItemsCount = 0
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItem.kt
index 21dbb69..16a5ac9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItem.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItem.kt
@@ -26,24 +26,38 @@
* if the user emit multiple layout nodes in the item callback.
*/
internal class LazyMeasuredItem(
+ override val index: Int,
private val placeables: List<Placeable>,
private val isVertical: Boolean,
- private val horizontalAlignment: Alignment.Horizontal,
- private val verticalAlignment: Alignment.Vertical,
+ private val horizontalAlignment: Alignment.Horizontal?,
+ private val verticalAlignment: Alignment.Vertical?,
private val layoutDirection: LayoutDirection,
private val startContentPadding: Int,
- private val endContentPadding: Int
-) {
+ private val endContentPadding: Int,
+ /**
+ * Extra spacing to be added to [size] aside from the sum of the [placeables] size. It
+ * is usually representing the spacing after the item.
+ */
+ private val spacing: Int
+) : LazyListItemInfo {
/**
* Sum of the main axis sizes of all the inner placeables.
*/
- val mainAxisSize: Int
+ override val size: Int
+
+ /**
+ * Sum of the main axis sizes of all the inner placeables and [spacing].
+ */
+ val sizeWithSpacings: Int
/**
* Max of the cross axis sizes of all the inner placeables.
*/
val crossAxisSize: Int
+ override var offset: Int = 0
+ private set
+
init {
var mainAxisSize = 0
var maxCrossAxis = 0
@@ -51,8 +65,9 @@
mainAxisSize += if (isVertical) it.height else it.width
maxCrossAxis = maxOf(maxCrossAxis, if (!isVertical) it.height else it.width)
}
- this.mainAxisSize = mainAxisSize
- this.crossAxisSize = maxCrossAxis
+ size = mainAxisSize
+ sizeWithSpacings = size + spacing
+ crossAxisSize = maxCrossAxis
}
/**
@@ -60,18 +75,23 @@
* and [layoutHeight] should be provided to not place placeables which are ended up outside of
* the viewport (for example one item consist of 2 placeables, and the first one is not going
* to be visible, so we don't place it as an optimization, but place the second one).
+ * If [reverseOrder] is true the inner placeables would be placed in the inverted order.
*/
fun place(
scope: Placeable.PlacementScope,
layoutWidth: Int,
layoutHeight: Int,
- offset: Int
+ offset: Int,
+ reverseOrder: Boolean
) = with(scope) {
+ this@LazyMeasuredItem.offset = offset
var mainAxisOffset = offset
- placeables.fastForEach {
+ val indices = if (reverseOrder) placeables.lastIndex downTo 0 else placeables.indices
+ for (index in indices) {
+ val it = placeables[index]
if (isVertical) {
- val x =
- horizontalAlignment.align(it.width, layoutWidth, layoutDirection)
+ val x = requireNotNull(horizontalAlignment)
+ .align(it.width, layoutWidth, layoutDirection)
if (mainAxisOffset + it.height > -startContentPadding &&
mainAxisOffset < layoutHeight + endContentPadding
) {
@@ -79,7 +99,7 @@
}
mainAxisOffset += it.height
} else {
- val y = verticalAlignment.align(it.height, layoutHeight)
+ val y = requireNotNull(verticalAlignment).align(it.height, layoutHeight)
if (mainAxisOffset + it.width > -startContentPadding &&
mainAxisOffset < layoutWidth + endContentPadding
) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItemProvider.kt
index ff038ff..89ae7d7 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItemProvider.kt
@@ -30,7 +30,7 @@
isVertical: Boolean,
private val scope: SubcomposeMeasureScope,
private val itemContentFactory: (Int) -> @Composable () -> Unit,
- private val measuredItemFactory: (List<Placeable>) -> LazyMeasuredItem
+ private val measuredItemFactory: (DataIndex, List<Placeable>) -> LazyMeasuredItem
) {
// the constraints we will measure child with. the main axis is not restricted
private val childConstraints = Constraints(
@@ -47,6 +47,6 @@
val placeables = scope.subcompose(index, itemContentFactory(index.value)).fastMap {
it.measure(childConstraints)
}
- return measuredItemFactory(placeables)
+ return measuredItemFactory(index, placeables)
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Selectable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Selectable.kt
index 22fd657d..a6df531 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Selectable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Selectable.kt
@@ -27,7 +27,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.semantics.accessibilityValue
+import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics
@@ -71,7 +71,7 @@
onClick = onClick
).semantics {
this.selected = selected
- this.accessibilityValue = if (selected) Strings.Selected else Strings.NotSelected
+ this.stateDescription = if (selected) Strings.Selected else Strings.NotSelected
}
},
inspectorInfo = debugInspectorInfo {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
index acde3bd..356f5e1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
@@ -30,7 +30,7 @@
import androidx.compose.ui.gesture.pressIndicatorGestureFilter
import androidx.compose.ui.gesture.tapGestureFilter
import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.semantics.accessibilityValue
+import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.semantics.disabled
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
@@ -137,7 +137,7 @@
): Modifier = composed {
// TODO(pavlis): Handle multiple states for Semantics
val semantics = Modifier.semantics(mergeDescendants = true) {
- this.accessibilityValue = when (state) {
+ this.stateDescription = when (state) {
// TODO(ryanmentley): These should be set by Checkbox, Switch, etc.
On -> Strings.Checked
Off -> Strings.Unchecked
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 a39fc33..8638678 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
@@ -52,6 +52,7 @@
import androidx.compose.ui.semantics.getTextLayoutResult
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.InternalTextApi
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.TextDelegate
@@ -96,6 +97,7 @@
*/
@Composable
@InternalTextApi
+@OptIn(ExperimentalTextApi::class)
fun CoreText(
text: AnnotatedString,
modifier: Modifier = Modifier,
@@ -194,7 +196,10 @@
}
}
-@OptIn(InternalTextApi::class)
+@OptIn(
+ InternalTextApi::class,
+ ExperimentalTextApi::class
+)
private class TextController(val state: TextState) {
var selectionRegistrar: SelectionRegistrar? = null
@@ -225,7 +230,7 @@
if (state.selectionRange != null) {
val newGlobalPosition = it.globalPosition
if (newGlobalPosition != state.previousGlobalPosition) {
- selectionRegistrar.onPositionChange()
+ selectionRegistrar.notifyPositionChange()
}
state.previousGlobalPosition = newGlobalPosition
}
@@ -353,7 +358,7 @@
var layoutCoordinates: LayoutCoordinates? = null
/** The latest TextLayoutResult calculated in the measure block */
var layoutResult: TextLayoutResult? = null
- /** The global position calculated during the last onPositioned callback */
+ /** The global position calculated during the last notifyPosition callback */
var previousGlobalPosition: Offset = Offset.Zero
/** The paint used to draw highlight background for selected text. */
val selectionPaint: Paint = Paint()
@@ -433,7 +438,10 @@
return Pair(placeholders, inlineComposables)
}
-@OptIn(InternalTextApi::class)
+@OptIn(
+ InternalTextApi::class,
+ ExperimentalTextApi::class
+)
@VisibleForTesting
internal fun longPressDragObserver(
state: TextState,
@@ -455,10 +463,9 @@
state.layoutCoordinates?.let {
if (!it.isAttached) return
- selectionRegistrar?.onUpdateSelection(
+ selectionRegistrar?.notifySelectionUpdateStart(
layoutCoordinates = it,
- startPosition = pxPosition,
- endPosition = pxPosition
+ startPosition = pxPosition
)
dragBeginPosition = pxPosition
@@ -466,7 +473,6 @@
}
override fun onDragStart() {
- super.onDragStart()
// selection never started
if (state.selectionRange == null) return
// Zero out the total distance that being dragged.
@@ -481,7 +487,7 @@
dragTotalDistance += dragDistance
- selectionRegistrar?.onUpdateSelection(
+ selectionRegistrar?.notifySelectionUpdate(
layoutCoordinates = it,
startPosition = dragBeginPosition,
endPosition = dragBeginPosition + dragTotalDistance
@@ -489,5 +495,13 @@
}
return dragDistance
}
+
+ override fun onStop(velocity: Offset) {
+ selectionRegistrar?.notifySelectionUpdateEnd()
+ }
+
+ override fun onCancel() {
+ selectionRegistrar?.notifySelectionUpdateEnd()
+ }
}
}
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 5ab2ff5..e3b4f32 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
@@ -32,7 +32,6 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.isFocused
import androidx.compose.ui.focusObserver
@@ -134,7 +133,6 @@
*/
@Composable
@OptIn(
- ExperimentalFocus::class,
ExperimentalTextApi::class,
MouseTemporaryApi::class
)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
index 626a730..e22c3ab 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldCursor.kt
@@ -18,11 +18,10 @@
import androidx.compose.animation.core.AnimatedFloat
import androidx.compose.animation.core.AnimationClockObservable
-import androidx.compose.animation.core.AnimationConstants
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes
-import androidx.compose.animation.core.repeatable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -103,8 +102,7 @@
}
private val cursorAnimationSpec: AnimationSpec<Float>
- get() = repeatable(
- iterations = AnimationConstants.Infinite,
+ get() = infiniteRepeatable(
animation = keyframes {
durationMillis = 1000
1f at 0
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
index eaa5cc2..f908eb0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
@@ -22,10 +22,12 @@
import androidx.compose.ui.selection.Selectable
import androidx.compose.ui.selection.Selection
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
import kotlin.math.max
+@OptIn(ExperimentalTextApi::class)
internal class MultiWidgetSelectionDelegate(
private val selectionRangeUpdate: (TextRange?) -> Unit,
private val coordinatesCallback: () -> LayoutCoordinates?,
@@ -123,6 +125,7 @@
*
* @return [Selection] of the current composable, or null if the composable is not selected.
*/
+@OptIn(ExperimentalTextApi::class)
internal fun getTextSelectionInfo(
textLayoutResult: TextLayoutResult,
selectionCoordinates: Pair<Offset, Offset>,
@@ -206,6 +209,7 @@
*
* @return [Selection] of the current composable, or null if the composable is not selected.
*/
+@OptIn(ExperimentalTextApi::class)
private fun getRefinedSelectionInfo(
rawStartOffset: Int,
rawEndOffset: Int,
@@ -282,6 +286,7 @@
*
* @return an assembled object of [Selection] using the offered selection info.
*/
+@OptIn(ExperimentalTextApi::class)
private fun getAssembledSelectionInfo(
startOffset: Int,
endOffset: Int,
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 89b5fd5..78c50d0 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
@@ -20,7 +20,6 @@
import androidx.compose.foundation.text.TextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
@@ -52,10 +51,7 @@
/**
* A bridge class between user interaction to the text field selection.
*/
-@OptIn(
- InternalTextApi::class,
- ExperimentalFocus::class
-)
+@OptIn(InternalTextApi::class)
internal class TextFieldSelectionManager() {
/**
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.kt
index 96e0375..0384857 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.kt
@@ -88,7 +88,7 @@
/**
* Vertical scrollbar that can be attached to some scrollable
- * component (ScrollableColumn, LazyColumnFor) and share common state with it.
+ * component (ScrollableColumn, LazyColumn) and share common state with it.
*
* Can be placed independently.
*
@@ -128,7 +128,7 @@
/**
* Horizontal scrollbar that can be attached to some scrollable
- * component (ScrollableRow, LazyRowFor) and share common state with it.
+ * component (ScrollableRow, LazyRow) and share common state with it.
*
* Can be placed independently.
*
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DesktopCoreTextField.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DesktopCoreTextField.kt
index 6b6caac..d5e4a44 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DesktopCoreTextField.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DesktopCoreTextField.kt
@@ -19,7 +19,6 @@
import androidx.compose.foundation.text.selection.TextFieldSelectionManager
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
-import androidx.compose.ui.input.key.ExperimentalKeyInput
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.plus
import androidx.compose.ui.input.key.shortcuts
@@ -40,7 +39,6 @@
private val selectAllKeySet by lazy { modifier + Key.A }
-@OptIn(ExperimentalKeyInput::class)
internal actual fun Modifier.textFieldKeyboardModifier(
manager: TextFieldSelectionManager
): Modifier = composed {
diff --git a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
index d2ed8a5..c2f7983 100644
--- a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
+++ b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
@@ -22,7 +22,7 @@
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumnFor
+import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
@@ -436,12 +436,13 @@
scrollbarWidth: Dp,
) = withTestEnvironment {
Box(Modifier.size(size)) {
- LazyColumnFor(
- (0 until childCount).toList(),
+ LazyColumn(
Modifier.fillMaxSize().testTag("column"),
state
) {
- Box(Modifier.size(childSize).testTag("box$it"))
+ items((0 until childCount).toList()) {
+ Box(Modifier.size(childSize).testTag("box$it"))
+ }
}
VerticalScrollbar(
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/DragGestureDetectorTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/DragGestureDetectorTest.kt
index a146012f..c9642e5 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/DragGestureDetectorTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/DragGestureDetectorTest.kt
@@ -14,12 +14,9 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalPointerInput::class)
-
package androidx.compose.foundation.gestures
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.ExperimentalPointerInput
import androidx.compose.ui.input.pointer.anyPositionChangeConsumed
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.input.pointer.consumeAllChanges
@@ -34,7 +31,6 @@
import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
-@OptIn(ExperimentalPointerInput::class)
class DragGestureDetectorTest(dragType: GestureType) {
enum class GestureType {
VerticalDrag,
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/MultitouchGestureDetectorTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/MultitouchGestureDetectorTest.kt
index e2a1373..aa3caaa 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/MultitouchGestureDetectorTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/MultitouchGestureDetectorTest.kt
@@ -14,12 +14,9 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalPointerInput::class)
-
package androidx.compose.foundation.gestures
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.ExperimentalPointerInput
import androidx.compose.ui.input.pointer.anyPositionChangeConsumed
import androidx.compose.ui.input.pointer.consumeAllChanges
import org.junit.Assert.assertEquals
@@ -31,7 +28,6 @@
import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
-@OptIn(ExperimentalPointerInput::class)
class MultitouchGestureDetectorTest(val panZoomLock: Boolean) {
companion object {
@JvmStatic
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt
index d3b5a40..1d8e90e 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt
@@ -29,7 +29,6 @@
import androidx.compose.runtime.withRunningRecomposer
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.ExperimentalPointerInput
import androidx.compose.ui.input.pointer.ConsumedData
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
@@ -59,7 +58,6 @@
* [gestureDetector]. The [width] and [height] of the LayoutNode may
* be provided.
*/
-@OptIn(ExperimentalPointerInput::class)
internal class SuspendingGestureTestUtil(
val width: Int = 10,
val height: Int = 10,
@@ -336,7 +334,10 @@
override val current: Unit = Unit
override fun down(node: Unit) {}
override fun up() {}
- override fun insert(index: Int, instance: Unit) {
+ override fun insertTopDown(index: Int, instance: Unit) {
+ error("Unexpected")
+ }
+ override fun insertBottomUp(index: Int, instance: Unit) {
error("Unexpected")
}
override fun remove(index: Int, count: Int) {
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
index a5740b8..5484742 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
@@ -14,12 +14,9 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalPointerInput::class)
-
package androidx.compose.foundation.gestures
import androidx.compose.ui.gesture.DoubleTapTimeout
-import androidx.compose.ui.gesture.ExperimentalPointerInput
import androidx.compose.ui.gesture.LongPressTimeout
import androidx.compose.ui.input.pointer.consumeDownChange
import androidx.compose.ui.input.pointer.consumePositionChange
@@ -34,7 +31,6 @@
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
-@OptIn(ExperimentalPointerInput::class)
class TapGestureDetectorTest {
private var pressed = false
private var released = false
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 9ea31ce..cd6e838 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
@@ -22,6 +22,7 @@
import androidx.compose.ui.selection.Selectable
import androidx.compose.ui.selection.Selection
import androidx.compose.ui.selection.SelectionRegistrar
+import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.InternalTextApi
import androidx.compose.ui.text.TextDelegate
import androidx.compose.ui.text.style.ResolvedTextDirection
@@ -36,7 +37,10 @@
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
-@OptIn(InternalTextApi::class)
+@OptIn(
+ InternalTextApi::class,
+ ExperimentalTextApi::class
+)
class TextSelectionLongPressDragTest {
private val selectionRegistrar = mock<SelectionRegistrar>()
private val selectable = mock<Selectable>()
@@ -93,15 +97,14 @@
}
@Test
- fun longPressDragObserver_onLongPress_calls_getSelection_change_selection() {
+ fun longPressDragObserver_onLongPress_calls_notifySelectionInitiated() {
val position = Offset(100f, 100f)
gesture.onLongPress(position)
- verify(selectionRegistrar, times(1)).onUpdateSelection(
+ verify(selectionRegistrar, times(1)).notifySelectionUpdateStart(
layoutCoordinates = layoutCoordinates,
- startPosition = position,
- endPosition = position
+ startPosition = position
)
}
@@ -127,7 +130,7 @@
// Verify.
verify(selectionRegistrar, times(1))
- .onUpdateSelection(
+ .notifySelectionUpdate(
layoutCoordinates = layoutCoordinates,
startPosition = beginPosition2,
endPosition = beginPosition2 + dragDistance2
@@ -135,7 +138,7 @@
}
@Test
- fun longPressDragObserver_onDrag_calls_getSelection_change_selection() {
+ fun longPressDragObserver_onDrag_calls_notifySelectionDrag() {
val dragDistance = Offset(15f, 10f)
val beginPosition = Offset(30f, 20f)
gesture.onLongPress(beginPosition)
@@ -146,10 +149,35 @@
assertThat(result).isEqualTo(dragDistance)
verify(selectionRegistrar, times(1))
- .onUpdateSelection(
+ .notifySelectionUpdate(
layoutCoordinates = layoutCoordinates,
startPosition = beginPosition,
endPosition = beginPosition + dragDistance
)
}
+
+ @Test
+ fun longPressDragObserver_onStop_calls_notifySelectionEnd() {
+ val dragDistance = Offset(15f, 10f)
+ val beginPosition = Offset(30f, 20f)
+ gesture.onLongPress(beginPosition)
+ state.selectionRange = fakeInitialSelection.toTextRange()
+ gesture.onDragStart()
+ gesture.onStop(dragDistance)
+
+ verify(selectionRegistrar, times(1))
+ .notifySelectionUpdateEnd()
+ }
+
+ @Test
+ fun longPressDragObserver_onCancel_calls_notifySelectionEnd() {
+ val beginPosition = Offset(30f, 20f)
+ gesture.onLongPress(beginPosition)
+ state.selectionRange = fakeInitialSelection.toTextRange()
+ gesture.onDragStart()
+ gesture.onCancel()
+
+ verify(selectionRegistrar, times(1))
+ .notifySelectionUpdateEnd()
+ }
}
\ No newline at end of file
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 78d7fff..e39145c 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,7 +17,6 @@
package androidx.compose.foundation.text.selection
import androidx.compose.foundation.text.TextFieldState
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
@@ -54,10 +53,7 @@
import org.mockito.stubbing.Answer
@RunWith(JUnit4::class)
-@OptIn(
- InternalTextApi::class,
- ExperimentalFocus::class
-)
+@OptIn(InternalTextApi::class)
class TextFieldSelectionManagerTest {
private val text = "Hello World"
private val density = Density(density = 1f)
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/lazy/LazyListScrollingBenchmark.kt b/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/lazy/LazyListScrollingBenchmark.kt
index 4c14493..8e652ac 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/lazy/LazyListScrollingBenchmark.kt
+++ b/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/lazy/LazyListScrollingBenchmark.kt
@@ -24,11 +24,7 @@
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyColumnFor
-import androidx.compose.foundation.lazy.LazyColumnForIndexed
import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.LazyRowFor
-import androidx.compose.foundation.lazy.LazyRowForIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.emptyContent
@@ -92,13 +88,9 @@
LazyColumnWithItemAndItems,
LazyColumnWithItems,
LazyColumnWithItemsIndexed,
- LazyColumnFor,
- LazyColumnForIndexed,
LazyRowWithItemAndItems,
LazyRowWithItems,
- LazyRowWithItemsIndexed,
- LazyRowFor,
- LazyRowForIndexed
+ LazyRowWithItemsIndexed
)
}
}
@@ -147,26 +139,6 @@
}
}
-private val LazyColumnFor = LazyListScrollingTestCase("LazyColumnFor") {
- LazyColumnFor(items, modifier = Modifier.height(400.dp).fillMaxWidth()) {
- if (it.index == 0) {
- RemeasurableItem()
- } else {
- RegularItem()
- }
- }
-}
-
-private val LazyColumnForIndexed = LazyListScrollingTestCase("LazyColumnForIndexed") {
- LazyColumnForIndexed(items, modifier = Modifier.height(400.dp).fillMaxWidth()) { index, _ ->
- if (index == 0) {
- RemeasurableItem()
- } else {
- RegularItem()
- }
- }
-}
-
private val LazyRowWithItemAndItems = LazyListScrollingTestCase("LazyRowWithItemAndItems") {
LazyRow(modifier = Modifier.width(400.dp).fillMaxHeight()) {
item {
@@ -202,26 +174,6 @@
}
}
-private val LazyRowFor = LazyListScrollingTestCase("LazyRowFor") {
- LazyRowFor(items, modifier = Modifier.width(400.dp).fillMaxHeight()) {
- if (it.index == 0) {
- RemeasurableItem()
- } else {
- RegularItem()
- }
- }
-}
-
-private val LazyRowForIndexed = LazyListScrollingTestCase("LazyRowForIndexed") {
- LazyRowForIndexed(items, modifier = Modifier.width(400.dp).fillMaxHeight()) { index, _ ->
- if (index == 0) {
- RemeasurableItem()
- } else {
- RegularItem()
- }
- }
-}
-
// TODO(b/169852102 use existing public constructs instead)
private fun ComposeBenchmarkRule.toggleStateBenchmarkMeasure(
caseFactory: () -> ListRemeasureTestCase
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/LayoutNodeModifierBenchmark.kt b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/LayoutNodeModifierBenchmark.kt
index c440dc1..079539b 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/LayoutNodeModifierBenchmark.kt
+++ b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/LayoutNodeModifierBenchmark.kt
@@ -14,28 +14,27 @@
* limitations under the License.
*/
+@file:Suppress("DEPRECATION_ERROR")
+
package androidx.ui.benchmark.test
-import android.view.ViewGroup
import androidx.activity.ComponentActivity
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.gesture.pressIndicatorGestureFilter
import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.input.key.ExperimentalKeyInput
import androidx.compose.ui.input.key.keyInputFilter
+import androidx.compose.ui.layout.TestModifierUpdater
+import androidx.compose.ui.layout.TestModifierUpdaterLayout
import androidx.compose.ui.layout.layoutId
-import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.AndroidOwner
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.test.junit4.DisableTransitionsTestRule
import androidx.compose.ui.test.InternalTestingApi
+import androidx.compose.ui.test.junit4.DisableTransitionsTestRule
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.test.filters.LargeTest
@@ -50,7 +49,7 @@
import org.junit.runners.model.Statement
/**
- * Benchmark that sets the [LayoutNode.modifier].
+ * Benchmark that sets the LayoutNode.modifier.
*/
@LargeTest
@RunWith(Parameterized::class)
@@ -68,10 +67,9 @@
var modifiers = emptyList<Modifier>()
var combinedModifier: Modifier = Modifier
- lateinit var layoutNode: LayoutNode
+ lateinit var testModifierUpdater: TestModifierUpdater
@Before
- @OptIn(ExperimentalKeyInput::class)
fun setup() {
modifiers = listOf(
Modifier.padding(10.dp),
@@ -91,14 +89,11 @@
}
rule.activityTestRule.runOnUiThread {
- rule.activityTestRule.activity.setContent { Box(Modifier) }
- }
- rule.activityTestRule.runOnUiThread {
- val composeView = rule.findAndroidOwner()
- val root = composeView.root
- check(root.children.size == 1) { "Expecting only a Box" }
- layoutNode = root.children[0]
- check(layoutNode.children.isEmpty()) { "Box should be empty" }
+ rule.activityTestRule.activity.setContent {
+ TestModifierUpdaterLayout {
+ testModifierUpdater = it
+ }
+ }
}
}
@@ -106,8 +101,8 @@
fun setAndClearModifiers() {
rule.activityTestRule.runOnUiThread {
rule.benchmarkRule.measureRepeated {
- layoutNode.modifier = combinedModifier
- layoutNode.modifier = Modifier
+ testModifierUpdater.updateModifier(combinedModifier)
+ testModifierUpdater.updateModifier(Modifier)
}
}
}
@@ -116,11 +111,11 @@
fun smallModifierChange() {
rule.activityTestRule.runOnUiThread {
val altModifier = Modifier.padding(10.dp).then(combinedModifier)
- layoutNode.modifier = altModifier
+ testModifierUpdater.updateModifier(altModifier)
rule.benchmarkRule.measureRepeated {
- layoutNode.modifier = combinedModifier
- layoutNode.modifier = altModifier
+ testModifierUpdater.updateModifier(combinedModifier)
+ testModifierUpdater.updateModifier(altModifier)
}
}
}
@@ -140,10 +135,5 @@
.around(activityTestRule)
.apply(base, description)
}
-
- fun findAndroidOwner(): AndroidOwner {
- return activityTestRule.activity.findViewById<ViewGroup>(android.R.id.content)
- .getChildAt(0) as AndroidOwner
- }
}
}
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/autofill/AndroidAutofillBenchmark.kt b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/autofill/AndroidAutofillBenchmark.kt
index 77859d6..4aee2ae 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/autofill/AndroidAutofillBenchmark.kt
+++ b/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/autofill/AndroidAutofillBenchmark.kt
@@ -21,6 +21,7 @@
import android.view.autofill.AutofillValue
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.autofill.AutofillNode
import androidx.compose.ui.autofill.AutofillTree
import androidx.compose.ui.autofill.AutofillType
@@ -38,6 +39,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
@LargeTest
+@OptIn(ExperimentalComposeUiApi::class)
@RunWith(AndroidJUnit4::class)
class AndroidAutofillBenchmark {
@@ -58,6 +60,7 @@
}
}
+ @OptIn(ExperimentalComposeUiApi::class)
@Test
@UiThreadTest
@SdkSuppress(minSdkVersion = 26)
diff --git a/compose/integration-tests/demos/build.gradle b/compose/integration-tests/demos/build.gradle
index 2f7c2c0..e422582 100644
--- a/compose/integration-tests/demos/build.gradle
+++ b/compose/integration-tests/demos/build.gradle
@@ -32,7 +32,7 @@
implementation project(":compose:runtime:runtime")
implementation project(":compose:ui:ui")
- implementation "androidx.preference:preference-ktx:1.1.0"
+ implementation "androidx.preference:preference-ktx:1.1.1"
androidTestImplementation project(":compose:ui:ui-test-junit4")
diff --git a/compose/integration-tests/demos/lint-baseline.xml b/compose/integration-tests/demos/lint-baseline.xml
deleted file mode 100644
index 08f1cf1..0000000
--- a/compose/integration-tests/demos/lint-baseline.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-alpha15" client="gradle" variant="debug" version="4.2.0-alpha15">
-
- <issue
- id="ObsoleteLintCustomCheck"
- message="Lint found an issue registry (`androidx.lifecycle.lint.LifecycleRuntimeIssueRegistry`) which is older than the current API level; these checks may not work correctly.

Recompile the checks against the latest version. Custom check API version is 6 (3.6), current lint API level is 8 (4.1)">
- <location
- file="../../../../../../../home/jeffrygaston/.gradle/caches/transforms-2/files-2.1/02dcda78691438fc76cdfd012889a28d/lifecycle-runtime-ktx-2.2.0/jars/lint.jar"/>
- </issue>
-
-</issues>
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoColors.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoColors.kt
index 4f908ce..5bb3529 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoColors.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoColors.kt
@@ -36,7 +36,6 @@
var light: Colors by mutableStateOf(lightColors())
var dark: Colors by mutableStateOf(darkColors())
- @Composable
val colors
- get() = if (isSystemInDarkTheme()) dark else light
+ @Composable get() = if (isSystemInDarkTheme()) dark else light
}
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
index 54eda68..7f2a927 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
@@ -39,7 +39,6 @@
import androidx.compose.runtime.onCommit
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focusRequester
import androidx.compose.ui.graphics.compositeOver
@@ -101,10 +100,7 @@
* [BasicTextField] that edits the current [filterText], providing [onFilter] when edited.
*/
@Composable
-@OptIn(
- ExperimentalFocus::class,
- ExperimentalFoundationApi::class
-)
+@OptIn(ExperimentalFoundationApi::class)
private fun FilterField(
filterText: String,
onFilter: (String) -> Unit,
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/Layout.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/Layout.kt
index dcab8ad..07e9583 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/Layout.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/layout/Layout.kt
@@ -29,7 +29,7 @@
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumnFor
+import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Button
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
@@ -137,8 +137,10 @@
onSelected: (Artist) -> Unit
) {
Surface(Modifier.fillMaxSize()) {
- LazyColumnFor(feedItems) { item ->
- ArtistCard(item, onSelected)
+ LazyColumn {
+ items(feedItems) { item ->
+ ArtistCard(item, onSelected)
+ }
}
}
}
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/mentalmodel/MentalModel.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/mentalmodel/MentalModel.kt
index e110e4d..635fbdf 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/mentalmodel/MentalModel.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/mentalmodel/MentalModel.kt
@@ -23,7 +23,7 @@
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.lazy.LazyColumnFor
+import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Button
import androidx.compose.material.Checkbox
import androidx.compose.material.Divider
@@ -118,12 +118,14 @@
Text(header, style = MaterialTheme.typography.h5)
Divider()
- // LazyColumnFor is the Compose version of a RecyclerView.
- // The lambda passed is similar to a RecyclerView.ViewHolder.
- LazyColumnFor(names) { name ->
- // When an item's [name] updates, the adapter for that item
- // will recompose. This will not recompose when [header] changes
- NamePickerItem(name, onNameClicked)
+ // LazyColumn is the Compose version of a RecyclerView.
+ // The lambda passed to items() is similar to a RecyclerView.ViewHolder.
+ LazyColumn {
+ items(names) { name ->
+ // When an item's [name] updates, the adapter for that item
+ // will recompose. This will not recompose when [header] changes
+ NamePickerItem(name, onNameClicked)
+ }
}
}
}
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/preview/LayoutPreview.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/preview/LayoutPreview.kt
index 3661374..25c3edd 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/preview/LayoutPreview.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/preview/LayoutPreview.kt
@@ -21,7 +21,7 @@
package androidx.compose.integration.docs.preview
import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants.defaultButtonColors
+import androidx.compose.material.ButtonDefaults.buttonColors
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
@@ -78,7 +78,7 @@
fun Counter(count: Int, updateCount: (Int) -> Unit) {
Button(
onClick = { updateCount(count + 1) },
- colors = defaultButtonColors(
+ colors = buttonColors(
backgroundColor = if (count > 5) Color.Green else Color.White
)
) {
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/state/State.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/state/State.kt
index 2c92a77..2dd4e4e 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/state/State.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/state/State.kt
@@ -327,7 +327,7 @@
private const val it = 1
private lateinit var helloViewModel: StateSnippet2.HelloViewModel
-private fun computeTextFormatting(st: String) {}
+private fun computeTextFormatting(st: String): String = ""
private fun ExpandingCard(
title: String,
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt
index a14e06bc..c5f7f64 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt
@@ -25,11 +25,10 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.key.ExperimentalKeyInput
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.semantics.AccessibilityAction
import androidx.compose.ui.semantics.SemanticsPropertyKey
-import androidx.compose.ui.semantics.accessibilityLabel
+import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteraction
@@ -47,10 +46,10 @@
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.test.onAllNodesWithLabel
+import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onChildren
import androidx.compose.ui.test.onFirst
-import androidx.compose.ui.test.onNodeWithLabel
+import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
@@ -73,7 +72,7 @@
*/
@Composable private fun TestingSnippet1() {
- MyButton(modifier = Modifier.semantics { accessibilityLabel = "Like button" })
+ MyButton(modifier = Modifier.semantics { contentDescription = "Like button" })
}
private object TestingSnippet3 {
@@ -142,11 +141,11 @@
@Composable private fun TestingSnippets8() {
// Check number of matched nodes
- composeTestRule.onAllNodesWithLabel("Beatle").assertCountEquals(4)
+ composeTestRule.onAllNodesWithContentDescription("Beatle").assertCountEquals(4)
// At least one matches
- composeTestRule.onAllNodesWithLabel("Beatle").assertAny(hasTestTag("Drummer"))
+ composeTestRule.onAllNodesWithContentDescription("Beatle").assertAny(hasTestTag("Drummer"))
// All of them match
- composeTestRule.onAllNodesWithLabel("Beatle").assertAll(hasClickAction())
+ composeTestRule.onAllNodesWithContentDescription("Beatle").assertAll(hasClickAction())
}
@Composable private fun SemanticsNodeInteraction.TestingSnippets9() {
@@ -202,13 +201,13 @@
@Test
fun changeTheme_scrollIsPersisted() {
- composeTestRule.onNodeWithLabel("Continue").performClick()
+ composeTestRule.onNodeWithContentDescription("Continue").performClick()
// Set theme to dark
themeIsDark.value = true
// Check that we're still on the same page
- composeTestRule.onNodeWithLabel("Welcome").assertIsDisplayed()
+ composeTestRule.onNodeWithContentDescription("Welcome").assertIsDisplayed()
}
}
}
@@ -228,5 +227,4 @@
private class MyActivity : ComponentActivity()
@Composable private fun MyButton(content: @Composable RowScope.() -> Unit) { }
private lateinit var key: SemanticsPropertyKey<AccessibilityAction<() -> Boolean>>
-@OptIn(ExperimentalKeyInput::class)
private lateinit var keyEvent: KeyEvent
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/theming/Theming.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/theming/Theming.kt
index 918997d..95a8359 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/theming/Theming.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/theming/Theming.kt
@@ -26,7 +26,7 @@
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.AmbientContentAlpha
import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants
+import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Colors
import androidx.compose.material.ContentAlpha
import androidx.compose.material.MaterialTheme
@@ -179,9 +179,8 @@
}
private object ThemingSnippet11 {
- @Composable
val Colors.snackbarAction: Color
- get() = if (isLight) Red300 else Red700
+ @Composable get() = if (isLight) Red300 else Red700
}
@Composable private fun ThemingSnippet12() {
@@ -251,7 +250,7 @@
content: @Composable RowScope.() -> Unit
) {
Button(
- colors = ButtonConstants.defaultButtonColors(
+ colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.secondary
),
onClick = onClick,
diff --git a/compose/integration-tests/macrobenchmark-target/build.gradle b/compose/integration-tests/macrobenchmark-target/build.gradle
index 7666c45..666cb1c 100644
--- a/compose/integration-tests/macrobenchmark-target/build.gradle
+++ b/compose/integration-tests/macrobenchmark-target/build.gradle
@@ -9,11 +9,20 @@
id("org.jetbrains.kotlin.android")
}
+android {
+ buildTypes {
+ release {
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
+ }
+ }
+}
+
dependencies {
kotlinPlugin project(":compose:compiler:compiler")
implementation(KOTLIN_STDLIB)
-
implementation project(":compose:foundation:foundation-layout")
implementation project(":compose:material:material")
implementation project(":compose:runtime:runtime")
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/LazyColumnActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/LazyColumnActivity.kt
index a0461f4..d981bab 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/LazyColumnActivity.kt
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/LazyColumnActivity.kt
@@ -21,11 +21,15 @@
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.lazy.LazyColumnFor
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.Card
import androidx.compose.material.Checkbox
import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.setContent
+import androidx.compose.ui.unit.dp
class LazyColumnActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -34,22 +38,12 @@
val itemCount = intent.getIntExtra(EXTRA_ITEM_COUNT, 1000)
setContent {
- LazyColumnFor(
- items = List(itemCount) {
- Entry("Item $it")
- },
- modifier = Modifier.fillMaxWidth(),
- itemContent = { item ->
- Row {
- Text(text = item.contents)
- Spacer(modifier = Modifier.weight(1f, fill = true))
- Checkbox(checked = false, onCheckedChange = {})
- }
+ LazyColumn(modifier = Modifier.fillMaxWidth()) {
+ items(List(itemCount) { Entry("Item $it") }) {
+ ListRow(it)
}
- )
+ }
}
-
- reportFullyDrawn()
}
companion object {
@@ -57,4 +51,22 @@
}
}
+@Composable
+private fun ListRow(entry: Entry) {
+ Card(modifier = Modifier.padding(8.dp)) {
+ Row {
+ Text(
+ text = entry.contents,
+ modifier = Modifier.padding(16.dp)
+ )
+ Spacer(modifier = Modifier.weight(1f, fill = true))
+ Checkbox(
+ checked = false,
+ onCheckedChange = {},
+ modifier = Modifier.padding(16.dp)
+ )
+ }
+ }
+}
+
data class Entry(val contents: String)
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/TrivialStartupActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/TrivialStartupActivity.kt
index ca32dc3..49dd58e 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/TrivialStartupActivity.kt
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/TrivialStartupActivity.kt
@@ -28,7 +28,5 @@
setContent {
Text("Compose Macrobenchmark Target")
}
-
- reportFullyDrawn()
}
}
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark/build.gradle b/compose/integration-tests/macrobenchmark/build.gradle
index 07444b7..1cf4c30 100644
--- a/compose/integration-tests/macrobenchmark/build.gradle
+++ b/compose/integration-tests/macrobenchmark/build.gradle
@@ -27,7 +27,10 @@
id("kotlin-android")
}
-android.defaultConfig.minSdkVersion 28
+android.defaultConfig {
+ minSdkVersion 28
+ testInstrumentationRunnerArgument 'androidx.benchmark.output.enable', 'true'
+}
dependencies {
androidTestImplementation(project(":benchmark:benchmark-junit4"))
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml b/compose/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
index 82ef367..b86f8ba 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/AndroidManifest.xml
@@ -28,4 +28,5 @@
<queries>
<package android:name="androidx.compose.integration.macrobenchmark.target" />
</queries>
+ <application android:requestLegacyExternalStorage="true"/>
</manifest>
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index a589728..866aca3 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -12,18 +12,32 @@
method @androidx.compose.runtime.Composable public static void TopAppBar-ye6PvEY(optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional float elevation, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
}
- public final class BackdropScaffoldConstants {
- method public float getDefaultFrontLayerElevation-D9Ej5fM();
- method public long getDefaultFrontLayerScrimColor-0d7_KjU();
- method public androidx.compose.ui.graphics.Shape getDefaultFrontLayerShape();
- method public float getDefaultHeaderHeight-D9Ej5fM();
- method public float getDefaultPeekHeight-D9Ej5fM();
+ @Deprecated public final class BackdropScaffoldConstants {
+ method @Deprecated public float getDefaultFrontLayerElevation-D9Ej5fM();
+ method @Deprecated @androidx.compose.runtime.Composable public long getDefaultFrontLayerScrimColor-0d7_KjU();
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getDefaultFrontLayerShape();
+ method @Deprecated public float getDefaultHeaderHeight-D9Ej5fM();
+ method @Deprecated public float getDefaultPeekHeight-D9Ej5fM();
property public final float DefaultFrontLayerElevation;
- property public final long DefaultFrontLayerScrimColor;
- property public final androidx.compose.ui.graphics.Shape DefaultFrontLayerShape;
+ property @androidx.compose.runtime.Composable public final long DefaultFrontLayerScrimColor;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape DefaultFrontLayerShape;
property public final float DefaultHeaderHeight;
property public final float DefaultPeekHeight;
- field public static final androidx.compose.material.BackdropScaffoldConstants INSTANCE;
+ field @Deprecated public static final androidx.compose.material.BackdropScaffoldConstants INSTANCE;
+ }
+
+ public final class BackdropScaffoldDefaults {
+ method public float getFrontLayerElevation-D9Ej5fM();
+ method @androidx.compose.runtime.Composable public long getFrontLayerScrimColor-0d7_KjU();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getFrontLayerShape();
+ method public float getHeaderHeight-D9Ej5fM();
+ method public float getPeekHeight-D9Ej5fM();
+ property public final float FrontLayerElevation;
+ property public final float HeaderHeight;
+ property public final float PeekHeight;
+ property @androidx.compose.runtime.Composable public final long frontLayerScrimColor;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape frontLayerShape;
+ field public static final androidx.compose.material.BackdropScaffoldDefaults INSTANCE;
}
public final class BackdropScaffoldKt {
@@ -82,12 +96,20 @@
method @androidx.compose.runtime.Composable public static void BottomNavigationItem-S1l6qvI(kotlin.jvm.functions.Function0<kotlin.Unit> icon, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> label, optional boolean alwaysShowLabels, optional androidx.compose.foundation.InteractionState interactionState, optional long selectedContentColor, optional long unselectedContentColor);
}
- public final class BottomSheetScaffoldConstants {
- method public float getDefaultSheetElevation-D9Ej5fM();
- method public float getDefaultSheetPeekHeight-D9Ej5fM();
+ @Deprecated public final class BottomSheetScaffoldConstants {
+ method @Deprecated public float getDefaultSheetElevation-D9Ej5fM();
+ method @Deprecated public float getDefaultSheetPeekHeight-D9Ej5fM();
property public final float DefaultSheetElevation;
property public final float DefaultSheetPeekHeight;
- field public static final androidx.compose.material.BottomSheetScaffoldConstants INSTANCE;
+ field @Deprecated public static final androidx.compose.material.BottomSheetScaffoldConstants INSTANCE;
+ }
+
+ public final class BottomSheetScaffoldDefaults {
+ method public float getSheetElevation-D9Ej5fM();
+ method public float getSheetPeekHeight-D9Ej5fM();
+ property public final float SheetElevation;
+ property public final float SheetPeekHeight;
+ field public static final androidx.compose.material.BottomSheetScaffoldDefaults INSTANCE;
}
public final class BottomSheetScaffoldKt {
@@ -131,19 +153,19 @@
method public long contentColor-0d7_KjU(boolean enabled);
}
- public final class ButtonConstants {
- method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultButtonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
- method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation defaultElevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
- method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultOutlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
- method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultTextButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
- method public androidx.compose.foundation.layout.PaddingValues getDefaultContentPadding();
- method public float getDefaultIconSize-D9Ej5fM();
- method public float getDefaultIconSpacing-D9Ej5fM();
- method public float getDefaultMinHeight-D9Ej5fM();
- method public float getDefaultMinWidth-D9Ej5fM();
- method public androidx.compose.foundation.BorderStroke getDefaultOutlinedBorder();
- method public androidx.compose.foundation.layout.PaddingValues getDefaultTextContentPadding();
- method public float getOutlinedBorderSize-D9Ej5fM();
+ @Deprecated public final class ButtonConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultButtonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation defaultElevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultOutlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultTextButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+ method @Deprecated public androidx.compose.foundation.layout.PaddingValues getDefaultContentPadding();
+ method @Deprecated public float getDefaultIconSize-D9Ej5fM();
+ method @Deprecated public float getDefaultIconSpacing-D9Ej5fM();
+ method @Deprecated public float getDefaultMinHeight-D9Ej5fM();
+ method @Deprecated public float getDefaultMinWidth-D9Ej5fM();
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke getDefaultOutlinedBorder();
+ method @Deprecated public androidx.compose.foundation.layout.PaddingValues getDefaultTextContentPadding();
+ method @Deprecated public float getOutlinedBorderSize-D9Ej5fM();
property public final androidx.compose.foundation.layout.PaddingValues DefaultContentPadding;
property public final float DefaultIconSize;
property public final float DefaultIconSpacing;
@@ -151,8 +173,33 @@
property public final float DefaultMinWidth;
property public final androidx.compose.foundation.layout.PaddingValues DefaultTextContentPadding;
property public final float OutlinedBorderSize;
- property public final androidx.compose.foundation.BorderStroke defaultOutlinedBorder;
- field public static final androidx.compose.material.ButtonConstants INSTANCE;
+ property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke defaultOutlinedBorder;
+ field @Deprecated public static final androidx.compose.material.ButtonConstants INSTANCE;
+ field @Deprecated public static final float OutlinedBorderOpacity = 0.12f;
+ }
+
+ public final class ButtonDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors buttonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation elevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
+ method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
+ method public float getIconSize-D9Ej5fM();
+ method public float getIconSpacing-D9Ej5fM();
+ method public float getMinHeight-D9Ej5fM();
+ method public float getMinWidth-D9Ej5fM();
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke getOutlinedBorder();
+ method public float getOutlinedBorderSize-D9Ej5fM();
+ method public androidx.compose.foundation.layout.PaddingValues getTextButtonContentPadding();
+ method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors outlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors textButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+ property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
+ property public final float IconSize;
+ property public final float IconSpacing;
+ property public final float MinHeight;
+ property public final float MinWidth;
+ property public final float OutlinedBorderSize;
+ property public final androidx.compose.foundation.layout.PaddingValues TextButtonContentPadding;
+ property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke outlinedBorder;
+ field public static final androidx.compose.material.ButtonDefaults INSTANCE;
field public static final float OutlinedBorderOpacity = 0.12f;
}
@@ -176,9 +223,14 @@
method public long checkmarkColor-0d7_KjU(androidx.compose.ui.state.ToggleableState state);
}
- public final class CheckboxConstants {
- method @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors defaultColors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
- field public static final androidx.compose.material.CheckboxConstants INSTANCE;
+ @Deprecated public final class CheckboxConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors defaultColors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
+ field @Deprecated public static final androidx.compose.material.CheckboxConstants INSTANCE;
+ }
+
+ public final class CheckboxDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors colors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
+ field public static final androidx.compose.material.CheckboxDefaults INSTANCE;
}
public final class CheckboxKt {
@@ -224,12 +276,12 @@
}
public final class ContentAlpha {
- method public float getDisabled();
- method public float getHigh();
- method public float getMedium();
- property public final float disabled;
- property public final float high;
- property public final float medium;
+ method @androidx.compose.runtime.Composable public float getDisabled();
+ method @androidx.compose.runtime.Composable public float getHigh();
+ method @androidx.compose.runtime.Composable public float getMedium();
+ property @androidx.compose.runtime.Composable public final float disabled;
+ property @androidx.compose.runtime.Composable public final float high;
+ property @androidx.compose.runtime.Composable public final float medium;
field public static final androidx.compose.material.ContentAlpha INSTANCE;
}
@@ -270,13 +322,22 @@
method @androidx.compose.runtime.Composable public static void Divider-JRSVyrs(optional androidx.compose.ui.Modifier modifier, optional long color, optional float thickness, optional float startIndent);
}
- public final class DrawerConstants {
- method public float getDefaultElevation-D9Ej5fM();
- method public long getDefaultScrimColor-0d7_KjU();
+ @Deprecated public final class DrawerConstants {
+ method @Deprecated public float getDefaultElevation-D9Ej5fM();
+ method @Deprecated @androidx.compose.runtime.Composable public long getDefaultScrimColor-0d7_KjU();
property public final float DefaultElevation;
- property public final long defaultScrimColor;
- field public static final androidx.compose.material.DrawerConstants INSTANCE;
- field public static final float ScrimDefaultOpacity = 0.32f;
+ property @androidx.compose.runtime.Composable public final long defaultScrimColor;
+ field @Deprecated public static final androidx.compose.material.DrawerConstants INSTANCE;
+ field @Deprecated public static final float ScrimDefaultOpacity = 0.32f;
+ }
+
+ public final class DrawerDefaults {
+ method public float getElevation-D9Ej5fM();
+ method @androidx.compose.runtime.Composable public long getScrimColor-0d7_KjU();
+ property public final float Elevation;
+ property @androidx.compose.runtime.Composable public final long scrimColor;
+ field public static final androidx.compose.material.DrawerDefaults INSTANCE;
+ field public static final float ScrimOpacity = 0.32f;
}
public final class DrawerKt {
@@ -306,10 +367,16 @@
enum_constant public static final androidx.compose.material.DrawerValue Open;
}
- public final class ElevationConstants {
+ @Deprecated public final class ElevationConstants {
+ method @Deprecated public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? incomingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
+ method @Deprecated public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? outgoingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
+ field @Deprecated public static final androidx.compose.material.ElevationConstants INSTANCE;
+ }
+
+ public final class ElevationDefaults {
method public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? incomingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
method public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? outgoingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
- field public static final androidx.compose.material.ElevationConstants INSTANCE;
+ field public static final androidx.compose.material.ElevationDefaults INSTANCE;
}
public final class ElevationKt {
@@ -335,12 +402,12 @@
}
@Deprecated public interface EmphasisLevels {
- method @Deprecated public androidx.compose.material.Emphasis getDisabled();
- method @Deprecated public androidx.compose.material.Emphasis getHigh();
- method @Deprecated public androidx.compose.material.Emphasis getMedium();
- property public abstract androidx.compose.material.Emphasis disabled;
- property public abstract androidx.compose.material.Emphasis high;
- property public abstract androidx.compose.material.Emphasis medium;
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getDisabled();
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getHigh();
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getMedium();
+ property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis disabled;
+ property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis high;
+ property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis medium;
}
@kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalMaterialApi {
@@ -356,9 +423,14 @@
method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public androidx.compose.material.FixedThreshold copy-0680j_4(float offset);
}
- public final class FloatingActionButtonConstants {
- method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation defaultElevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
- field public static final androidx.compose.material.FloatingActionButtonConstants INSTANCE;
+ @Deprecated public final class FloatingActionButtonConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation defaultElevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
+ field @Deprecated public static final androidx.compose.material.FloatingActionButtonConstants INSTANCE;
+ }
+
+ public final class FloatingActionButtonDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation elevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
+ field public static final androidx.compose.material.FloatingActionButtonDefaults INSTANCE;
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface FloatingActionButtonElevation {
@@ -395,12 +467,12 @@
}
public final class MaterialTheme {
- method public androidx.compose.material.Colors getColors();
- method public androidx.compose.material.Shapes getShapes();
- method public androidx.compose.material.Typography getTypography();
- property public final androidx.compose.material.Colors colors;
- property public final androidx.compose.material.Shapes shapes;
- property public final androidx.compose.material.Typography typography;
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public androidx.compose.material.Colors getColors();
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public androidx.compose.material.Shapes getShapes();
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public androidx.compose.material.Typography getTypography();
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final androidx.compose.material.Colors colors;
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final androidx.compose.material.Shapes shapes;
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final androidx.compose.material.Typography typography;
field public static final androidx.compose.material.MaterialTheme INSTANCE;
}
@@ -413,12 +485,20 @@
method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.InteractionState interactionState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
- public final class ModalBottomSheetConstants {
- method public float getDefaultElevation-D9Ej5fM();
- method public long getDefaultScrimColor-0d7_KjU();
+ @Deprecated public final class ModalBottomSheetConstants {
+ method @Deprecated public float getDefaultElevation-D9Ej5fM();
+ method @Deprecated @androidx.compose.runtime.Composable public long getDefaultScrimColor-0d7_KjU();
property public final float DefaultElevation;
- property public final long DefaultScrimColor;
- field public static final androidx.compose.material.ModalBottomSheetConstants INSTANCE;
+ property @androidx.compose.runtime.Composable public final long DefaultScrimColor;
+ field @Deprecated public static final androidx.compose.material.ModalBottomSheetConstants INSTANCE;
+ }
+
+ public final class ModalBottomSheetDefaults {
+ method public float getElevation-D9Ej5fM();
+ method @androidx.compose.runtime.Composable public long getScrimColor-0d7_KjU();
+ property public final float Elevation;
+ property @androidx.compose.runtime.Composable public final long scrimColor;
+ field public static final androidx.compose.material.ModalBottomSheetDefaults INSTANCE;
}
public final class ModalBottomSheetKt {
@@ -450,13 +530,22 @@
method @androidx.compose.runtime.Composable public static void OutlinedTextField-IIju55g(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 androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isErrorValue, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.text.input.ImeAction,? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onImeActionPerformed, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long activeColor, optional long inactiveColor, optional long errorColor);
}
- public final class ProgressIndicatorConstants {
- method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultProgressAnimationSpec();
- method public float getDefaultStrokeWidth-D9Ej5fM();
+ @Deprecated public final class ProgressIndicatorConstants {
+ method @Deprecated public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultProgressAnimationSpec();
+ method @Deprecated public float getDefaultStrokeWidth-D9Ej5fM();
property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> DefaultProgressAnimationSpec;
property public final float DefaultStrokeWidth;
- field public static final float DefaultIndicatorBackgroundOpacity = 0.24f;
- field public static final androidx.compose.material.ProgressIndicatorConstants INSTANCE;
+ field @Deprecated public static final float DefaultIndicatorBackgroundOpacity = 0.24f;
+ field @Deprecated public static final androidx.compose.material.ProgressIndicatorConstants INSTANCE;
+ }
+
+ public final class ProgressIndicatorDefaults {
+ method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getProgressAnimationSpec();
+ method public float getStrokeWidth-D9Ej5fM();
+ property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> ProgressAnimationSpec;
+ property public final float StrokeWidth;
+ field public static final androidx.compose.material.ProgressIndicatorDefaults INSTANCE;
+ field public static final float IndicatorBackgroundOpacity = 0.24f;
}
public final class ProgressIndicatorKt {
@@ -470,9 +559,14 @@
method public long radioColor-0d7_KjU(boolean enabled, boolean selected);
}
- public final class RadioButtonConstants {
- method @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors defaultColors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
- field public static final androidx.compose.material.RadioButtonConstants INSTANCE;
+ @Deprecated public final class RadioButtonConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors defaultColors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
+ field @Deprecated public static final androidx.compose.material.RadioButtonConstants INSTANCE;
+ }
+
+ public final class RadioButtonDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors colors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
+ field public static final androidx.compose.material.RadioButtonDefaults INSTANCE;
}
public final class RadioButtonKt {
@@ -525,8 +619,14 @@
public final class ShapesKt {
}
- public final class SliderConstants {
- field public static final androidx.compose.material.SliderConstants INSTANCE;
+ @Deprecated public final class SliderConstants {
+ field @Deprecated public static final androidx.compose.material.SliderConstants INSTANCE;
+ field @Deprecated public static final float InactiveTrackColorAlpha = 0.24f;
+ field @Deprecated public static final float TickColorAlpha = 0.54f;
+ }
+
+ public final class SliderDefaults {
+ field public static final androidx.compose.material.SliderDefaults INSTANCE;
field public static final float InactiveTrackColorAlpha = 0.24f;
field public static final float TickColorAlpha = 0.54f;
}
@@ -535,12 +635,12 @@
method @androidx.compose.runtime.Composable public static void Slider-B7rb6FQ(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional @IntRange(from=0) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit> onValueChangeEnd, optional androidx.compose.foundation.InteractionState interactionState, optional long thumbColor, optional long activeTrackColor, optional long inactiveTrackColor, optional long activeTickColor, optional long inactiveTickColor);
}
- public final class SnackbarConstants {
- method public long getDefaultActionPrimaryColor-0d7_KjU();
- method public long getDefaultBackgroundColor-0d7_KjU();
- property public final long defaultActionPrimaryColor;
- property public final long defaultBackgroundColor;
- field public static final androidx.compose.material.SnackbarConstants INSTANCE;
+ @Deprecated public final class SnackbarConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public long getDefaultActionPrimaryColor-0d7_KjU();
+ method @Deprecated @androidx.compose.runtime.Composable public long getDefaultBackgroundColor-0d7_KjU();
+ property @androidx.compose.runtime.Composable public final long defaultActionPrimaryColor;
+ property @androidx.compose.runtime.Composable public final long defaultBackgroundColor;
+ field @Deprecated public static final androidx.compose.material.SnackbarConstants INSTANCE;
}
@androidx.compose.material.ExperimentalMaterialApi public interface SnackbarData {
@@ -554,6 +654,14 @@
property public abstract String message;
}
+ public final class SnackbarDefaults {
+ method @androidx.compose.runtime.Composable public long getBackgroundColor-0d7_KjU();
+ method @androidx.compose.runtime.Composable public long getPrimaryActionColor-0d7_KjU();
+ property @androidx.compose.runtime.Composable public final long backgroundColor;
+ property @androidx.compose.runtime.Composable public final long primaryActionColor;
+ field public static final androidx.compose.material.SnackbarDefaults INSTANCE;
+ }
+
public enum SnackbarDuration {
enum_constant public static final androidx.compose.material.SnackbarDuration Indefinite;
enum_constant public static final androidx.compose.material.SnackbarDuration Long;
@@ -605,13 +713,24 @@
method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.DismissState rememberDismissState(optional androidx.compose.material.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DismissValue,java.lang.Boolean> confirmStateChange);
}
- public final class SwipeableConstants {
- method public androidx.compose.material.ResistanceConfig? defaultResistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
- method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultAnimationSpec();
- method public float getDefaultVelocityThreshold-D9Ej5fM();
+ @Deprecated public final class SwipeableConstants {
+ method @Deprecated public androidx.compose.material.ResistanceConfig? defaultResistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
+ method @Deprecated public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultAnimationSpec();
+ method @Deprecated public float getDefaultVelocityThreshold-D9Ej5fM();
property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> DefaultAnimationSpec;
property public final float DefaultVelocityThreshold;
- field public static final androidx.compose.material.SwipeableConstants INSTANCE;
+ field @Deprecated public static final androidx.compose.material.SwipeableConstants INSTANCE;
+ field @Deprecated public static final float StandardResistanceFactor = 10.0f;
+ field @Deprecated public static final float StiffResistanceFactor = 20.0f;
+ }
+
+ public final class SwipeableDefaults {
+ method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getAnimationSpec();
+ method public float getVelocityThreshold-D9Ej5fM();
+ method public androidx.compose.material.ResistanceConfig? resistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
+ property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> AnimationSpec;
+ property public final float VelocityThreshold;
+ field public static final androidx.compose.material.SwipeableDefaults INSTANCE;
field public static final float StandardResistanceFactor = 10.0f;
field public static final float StiffResistanceFactor = 20.0f;
}
@@ -631,6 +750,8 @@
method public final T! getTargetValue();
method public final T! getValue();
method public final boolean isAnimationRunning();
+ method public final float performDrag(float delta);
+ method public final void performFling(float velocity, kotlin.jvm.functions.Function0<kotlin.Unit> onEnd);
method @androidx.compose.material.ExperimentalMaterialApi public final void snapTo(T? targetValue);
property public final float direction;
property public final boolean isAnimationRunning;
@@ -651,27 +772,46 @@
method public long trackColor-0d7_KjU(boolean enabled, boolean checked);
}
- public final class SwitchConstants {
- method @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors defaultColors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
- field public static final androidx.compose.material.SwitchConstants INSTANCE;
+ @Deprecated public final class SwitchConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors defaultColors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
+ field @Deprecated public static final androidx.compose.material.SwitchConstants INSTANCE;
+ }
+
+ public final class SwitchDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors colors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
+ field public static final androidx.compose.material.SwitchDefaults INSTANCE;
}
public final class SwitchKt {
method @androidx.compose.runtime.Composable public static void Switch(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.InteractionState interactionState, optional androidx.compose.material.SwitchColors colors);
}
- public final class TabConstants {
- method @androidx.compose.runtime.Composable public void DefaultDivider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
- method @androidx.compose.runtime.Composable public void DefaultIndicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
- method public androidx.compose.ui.Modifier defaultTabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
- method public float getDefaultDividerThickness-D9Ej5fM();
- method public float getDefaultIndicatorHeight-D9Ej5fM();
- method public float getDefaultScrollableTabRowPadding-D9Ej5fM();
+ @Deprecated public final class TabConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public void DefaultDivider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
+ method @Deprecated @androidx.compose.runtime.Composable public void DefaultIndicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+ method @Deprecated public androidx.compose.ui.Modifier defaultTabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
+ method @Deprecated public float getDefaultDividerThickness-D9Ej5fM();
+ method @Deprecated public float getDefaultIndicatorHeight-D9Ej5fM();
+ method @Deprecated public float getDefaultScrollableTabRowPadding-D9Ej5fM();
property public final float DefaultDividerThickness;
property public final float DefaultIndicatorHeight;
property public final float DefaultScrollableTabRowPadding;
- field public static final float DefaultDividerOpacity = 0.12f;
- field public static final androidx.compose.material.TabConstants INSTANCE;
+ field @Deprecated public static final float DefaultDividerOpacity = 0.12f;
+ field @Deprecated public static final androidx.compose.material.TabConstants INSTANCE;
+ }
+
+ public final class TabDefaults {
+ method @androidx.compose.runtime.Composable public void Divider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
+ method @androidx.compose.runtime.Composable public void Indicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+ method public float getDividerThickness-D9Ej5fM();
+ method public float getIndicatorHeight-D9Ej5fM();
+ method public float getScrollableTabRowPadding-D9Ej5fM();
+ method public androidx.compose.ui.Modifier tabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
+ property public final float DividerThickness;
+ property public final float IndicatorHeight;
+ property public final float ScrollableTabRowPadding;
+ field public static final float DividerOpacity = 0.12f;
+ field public static final androidx.compose.material.TabDefaults INSTANCE;
}
public final class TabKt {
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index a589728..866aca3 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -12,18 +12,32 @@
method @androidx.compose.runtime.Composable public static void TopAppBar-ye6PvEY(optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional float elevation, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
}
- public final class BackdropScaffoldConstants {
- method public float getDefaultFrontLayerElevation-D9Ej5fM();
- method public long getDefaultFrontLayerScrimColor-0d7_KjU();
- method public androidx.compose.ui.graphics.Shape getDefaultFrontLayerShape();
- method public float getDefaultHeaderHeight-D9Ej5fM();
- method public float getDefaultPeekHeight-D9Ej5fM();
+ @Deprecated public final class BackdropScaffoldConstants {
+ method @Deprecated public float getDefaultFrontLayerElevation-D9Ej5fM();
+ method @Deprecated @androidx.compose.runtime.Composable public long getDefaultFrontLayerScrimColor-0d7_KjU();
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getDefaultFrontLayerShape();
+ method @Deprecated public float getDefaultHeaderHeight-D9Ej5fM();
+ method @Deprecated public float getDefaultPeekHeight-D9Ej5fM();
property public final float DefaultFrontLayerElevation;
- property public final long DefaultFrontLayerScrimColor;
- property public final androidx.compose.ui.graphics.Shape DefaultFrontLayerShape;
+ property @androidx.compose.runtime.Composable public final long DefaultFrontLayerScrimColor;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape DefaultFrontLayerShape;
property public final float DefaultHeaderHeight;
property public final float DefaultPeekHeight;
- field public static final androidx.compose.material.BackdropScaffoldConstants INSTANCE;
+ field @Deprecated public static final androidx.compose.material.BackdropScaffoldConstants INSTANCE;
+ }
+
+ public final class BackdropScaffoldDefaults {
+ method public float getFrontLayerElevation-D9Ej5fM();
+ method @androidx.compose.runtime.Composable public long getFrontLayerScrimColor-0d7_KjU();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getFrontLayerShape();
+ method public float getHeaderHeight-D9Ej5fM();
+ method public float getPeekHeight-D9Ej5fM();
+ property public final float FrontLayerElevation;
+ property public final float HeaderHeight;
+ property public final float PeekHeight;
+ property @androidx.compose.runtime.Composable public final long frontLayerScrimColor;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape frontLayerShape;
+ field public static final androidx.compose.material.BackdropScaffoldDefaults INSTANCE;
}
public final class BackdropScaffoldKt {
@@ -82,12 +96,20 @@
method @androidx.compose.runtime.Composable public static void BottomNavigationItem-S1l6qvI(kotlin.jvm.functions.Function0<kotlin.Unit> icon, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> label, optional boolean alwaysShowLabels, optional androidx.compose.foundation.InteractionState interactionState, optional long selectedContentColor, optional long unselectedContentColor);
}
- public final class BottomSheetScaffoldConstants {
- method public float getDefaultSheetElevation-D9Ej5fM();
- method public float getDefaultSheetPeekHeight-D9Ej5fM();
+ @Deprecated public final class BottomSheetScaffoldConstants {
+ method @Deprecated public float getDefaultSheetElevation-D9Ej5fM();
+ method @Deprecated public float getDefaultSheetPeekHeight-D9Ej5fM();
property public final float DefaultSheetElevation;
property public final float DefaultSheetPeekHeight;
- field public static final androidx.compose.material.BottomSheetScaffoldConstants INSTANCE;
+ field @Deprecated public static final androidx.compose.material.BottomSheetScaffoldConstants INSTANCE;
+ }
+
+ public final class BottomSheetScaffoldDefaults {
+ method public float getSheetElevation-D9Ej5fM();
+ method public float getSheetPeekHeight-D9Ej5fM();
+ property public final float SheetElevation;
+ property public final float SheetPeekHeight;
+ field public static final androidx.compose.material.BottomSheetScaffoldDefaults INSTANCE;
}
public final class BottomSheetScaffoldKt {
@@ -131,19 +153,19 @@
method public long contentColor-0d7_KjU(boolean enabled);
}
- public final class ButtonConstants {
- method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultButtonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
- method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation defaultElevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
- method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultOutlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
- method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultTextButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
- method public androidx.compose.foundation.layout.PaddingValues getDefaultContentPadding();
- method public float getDefaultIconSize-D9Ej5fM();
- method public float getDefaultIconSpacing-D9Ej5fM();
- method public float getDefaultMinHeight-D9Ej5fM();
- method public float getDefaultMinWidth-D9Ej5fM();
- method public androidx.compose.foundation.BorderStroke getDefaultOutlinedBorder();
- method public androidx.compose.foundation.layout.PaddingValues getDefaultTextContentPadding();
- method public float getOutlinedBorderSize-D9Ej5fM();
+ @Deprecated public final class ButtonConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultButtonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation defaultElevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultOutlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultTextButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+ method @Deprecated public androidx.compose.foundation.layout.PaddingValues getDefaultContentPadding();
+ method @Deprecated public float getDefaultIconSize-D9Ej5fM();
+ method @Deprecated public float getDefaultIconSpacing-D9Ej5fM();
+ method @Deprecated public float getDefaultMinHeight-D9Ej5fM();
+ method @Deprecated public float getDefaultMinWidth-D9Ej5fM();
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke getDefaultOutlinedBorder();
+ method @Deprecated public androidx.compose.foundation.layout.PaddingValues getDefaultTextContentPadding();
+ method @Deprecated public float getOutlinedBorderSize-D9Ej5fM();
property public final androidx.compose.foundation.layout.PaddingValues DefaultContentPadding;
property public final float DefaultIconSize;
property public final float DefaultIconSpacing;
@@ -151,8 +173,33 @@
property public final float DefaultMinWidth;
property public final androidx.compose.foundation.layout.PaddingValues DefaultTextContentPadding;
property public final float OutlinedBorderSize;
- property public final androidx.compose.foundation.BorderStroke defaultOutlinedBorder;
- field public static final androidx.compose.material.ButtonConstants INSTANCE;
+ property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke defaultOutlinedBorder;
+ field @Deprecated public static final androidx.compose.material.ButtonConstants INSTANCE;
+ field @Deprecated public static final float OutlinedBorderOpacity = 0.12f;
+ }
+
+ public final class ButtonDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors buttonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation elevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
+ method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
+ method public float getIconSize-D9Ej5fM();
+ method public float getIconSpacing-D9Ej5fM();
+ method public float getMinHeight-D9Ej5fM();
+ method public float getMinWidth-D9Ej5fM();
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke getOutlinedBorder();
+ method public float getOutlinedBorderSize-D9Ej5fM();
+ method public androidx.compose.foundation.layout.PaddingValues getTextButtonContentPadding();
+ method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors outlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors textButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+ property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
+ property public final float IconSize;
+ property public final float IconSpacing;
+ property public final float MinHeight;
+ property public final float MinWidth;
+ property public final float OutlinedBorderSize;
+ property public final androidx.compose.foundation.layout.PaddingValues TextButtonContentPadding;
+ property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke outlinedBorder;
+ field public static final androidx.compose.material.ButtonDefaults INSTANCE;
field public static final float OutlinedBorderOpacity = 0.12f;
}
@@ -176,9 +223,14 @@
method public long checkmarkColor-0d7_KjU(androidx.compose.ui.state.ToggleableState state);
}
- public final class CheckboxConstants {
- method @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors defaultColors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
- field public static final androidx.compose.material.CheckboxConstants INSTANCE;
+ @Deprecated public final class CheckboxConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors defaultColors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
+ field @Deprecated public static final androidx.compose.material.CheckboxConstants INSTANCE;
+ }
+
+ public final class CheckboxDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors colors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
+ field public static final androidx.compose.material.CheckboxDefaults INSTANCE;
}
public final class CheckboxKt {
@@ -224,12 +276,12 @@
}
public final class ContentAlpha {
- method public float getDisabled();
- method public float getHigh();
- method public float getMedium();
- property public final float disabled;
- property public final float high;
- property public final float medium;
+ method @androidx.compose.runtime.Composable public float getDisabled();
+ method @androidx.compose.runtime.Composable public float getHigh();
+ method @androidx.compose.runtime.Composable public float getMedium();
+ property @androidx.compose.runtime.Composable public final float disabled;
+ property @androidx.compose.runtime.Composable public final float high;
+ property @androidx.compose.runtime.Composable public final float medium;
field public static final androidx.compose.material.ContentAlpha INSTANCE;
}
@@ -270,13 +322,22 @@
method @androidx.compose.runtime.Composable public static void Divider-JRSVyrs(optional androidx.compose.ui.Modifier modifier, optional long color, optional float thickness, optional float startIndent);
}
- public final class DrawerConstants {
- method public float getDefaultElevation-D9Ej5fM();
- method public long getDefaultScrimColor-0d7_KjU();
+ @Deprecated public final class DrawerConstants {
+ method @Deprecated public float getDefaultElevation-D9Ej5fM();
+ method @Deprecated @androidx.compose.runtime.Composable public long getDefaultScrimColor-0d7_KjU();
property public final float DefaultElevation;
- property public final long defaultScrimColor;
- field public static final androidx.compose.material.DrawerConstants INSTANCE;
- field public static final float ScrimDefaultOpacity = 0.32f;
+ property @androidx.compose.runtime.Composable public final long defaultScrimColor;
+ field @Deprecated public static final androidx.compose.material.DrawerConstants INSTANCE;
+ field @Deprecated public static final float ScrimDefaultOpacity = 0.32f;
+ }
+
+ public final class DrawerDefaults {
+ method public float getElevation-D9Ej5fM();
+ method @androidx.compose.runtime.Composable public long getScrimColor-0d7_KjU();
+ property public final float Elevation;
+ property @androidx.compose.runtime.Composable public final long scrimColor;
+ field public static final androidx.compose.material.DrawerDefaults INSTANCE;
+ field public static final float ScrimOpacity = 0.32f;
}
public final class DrawerKt {
@@ -306,10 +367,16 @@
enum_constant public static final androidx.compose.material.DrawerValue Open;
}
- public final class ElevationConstants {
+ @Deprecated public final class ElevationConstants {
+ method @Deprecated public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? incomingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
+ method @Deprecated public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? outgoingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
+ field @Deprecated public static final androidx.compose.material.ElevationConstants INSTANCE;
+ }
+
+ public final class ElevationDefaults {
method public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? incomingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
method public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? outgoingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
- field public static final androidx.compose.material.ElevationConstants INSTANCE;
+ field public static final androidx.compose.material.ElevationDefaults INSTANCE;
}
public final class ElevationKt {
@@ -335,12 +402,12 @@
}
@Deprecated public interface EmphasisLevels {
- method @Deprecated public androidx.compose.material.Emphasis getDisabled();
- method @Deprecated public androidx.compose.material.Emphasis getHigh();
- method @Deprecated public androidx.compose.material.Emphasis getMedium();
- property public abstract androidx.compose.material.Emphasis disabled;
- property public abstract androidx.compose.material.Emphasis high;
- property public abstract androidx.compose.material.Emphasis medium;
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getDisabled();
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getHigh();
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getMedium();
+ property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis disabled;
+ property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis high;
+ property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis medium;
}
@kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalMaterialApi {
@@ -356,9 +423,14 @@
method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public androidx.compose.material.FixedThreshold copy-0680j_4(float offset);
}
- public final class FloatingActionButtonConstants {
- method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation defaultElevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
- field public static final androidx.compose.material.FloatingActionButtonConstants INSTANCE;
+ @Deprecated public final class FloatingActionButtonConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation defaultElevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
+ field @Deprecated public static final androidx.compose.material.FloatingActionButtonConstants INSTANCE;
+ }
+
+ public final class FloatingActionButtonDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation elevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
+ field public static final androidx.compose.material.FloatingActionButtonDefaults INSTANCE;
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface FloatingActionButtonElevation {
@@ -395,12 +467,12 @@
}
public final class MaterialTheme {
- method public androidx.compose.material.Colors getColors();
- method public androidx.compose.material.Shapes getShapes();
- method public androidx.compose.material.Typography getTypography();
- property public final androidx.compose.material.Colors colors;
- property public final androidx.compose.material.Shapes shapes;
- property public final androidx.compose.material.Typography typography;
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public androidx.compose.material.Colors getColors();
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public androidx.compose.material.Shapes getShapes();
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public androidx.compose.material.Typography getTypography();
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final androidx.compose.material.Colors colors;
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final androidx.compose.material.Shapes shapes;
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final androidx.compose.material.Typography typography;
field public static final androidx.compose.material.MaterialTheme INSTANCE;
}
@@ -413,12 +485,20 @@
method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.InteractionState interactionState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
- public final class ModalBottomSheetConstants {
- method public float getDefaultElevation-D9Ej5fM();
- method public long getDefaultScrimColor-0d7_KjU();
+ @Deprecated public final class ModalBottomSheetConstants {
+ method @Deprecated public float getDefaultElevation-D9Ej5fM();
+ method @Deprecated @androidx.compose.runtime.Composable public long getDefaultScrimColor-0d7_KjU();
property public final float DefaultElevation;
- property public final long DefaultScrimColor;
- field public static final androidx.compose.material.ModalBottomSheetConstants INSTANCE;
+ property @androidx.compose.runtime.Composable public final long DefaultScrimColor;
+ field @Deprecated public static final androidx.compose.material.ModalBottomSheetConstants INSTANCE;
+ }
+
+ public final class ModalBottomSheetDefaults {
+ method public float getElevation-D9Ej5fM();
+ method @androidx.compose.runtime.Composable public long getScrimColor-0d7_KjU();
+ property public final float Elevation;
+ property @androidx.compose.runtime.Composable public final long scrimColor;
+ field public static final androidx.compose.material.ModalBottomSheetDefaults INSTANCE;
}
public final class ModalBottomSheetKt {
@@ -450,13 +530,22 @@
method @androidx.compose.runtime.Composable public static void OutlinedTextField-IIju55g(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 androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isErrorValue, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.text.input.ImeAction,? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onImeActionPerformed, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long activeColor, optional long inactiveColor, optional long errorColor);
}
- public final class ProgressIndicatorConstants {
- method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultProgressAnimationSpec();
- method public float getDefaultStrokeWidth-D9Ej5fM();
+ @Deprecated public final class ProgressIndicatorConstants {
+ method @Deprecated public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultProgressAnimationSpec();
+ method @Deprecated public float getDefaultStrokeWidth-D9Ej5fM();
property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> DefaultProgressAnimationSpec;
property public final float DefaultStrokeWidth;
- field public static final float DefaultIndicatorBackgroundOpacity = 0.24f;
- field public static final androidx.compose.material.ProgressIndicatorConstants INSTANCE;
+ field @Deprecated public static final float DefaultIndicatorBackgroundOpacity = 0.24f;
+ field @Deprecated public static final androidx.compose.material.ProgressIndicatorConstants INSTANCE;
+ }
+
+ public final class ProgressIndicatorDefaults {
+ method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getProgressAnimationSpec();
+ method public float getStrokeWidth-D9Ej5fM();
+ property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> ProgressAnimationSpec;
+ property public final float StrokeWidth;
+ field public static final androidx.compose.material.ProgressIndicatorDefaults INSTANCE;
+ field public static final float IndicatorBackgroundOpacity = 0.24f;
}
public final class ProgressIndicatorKt {
@@ -470,9 +559,14 @@
method public long radioColor-0d7_KjU(boolean enabled, boolean selected);
}
- public final class RadioButtonConstants {
- method @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors defaultColors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
- field public static final androidx.compose.material.RadioButtonConstants INSTANCE;
+ @Deprecated public final class RadioButtonConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors defaultColors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
+ field @Deprecated public static final androidx.compose.material.RadioButtonConstants INSTANCE;
+ }
+
+ public final class RadioButtonDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors colors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
+ field public static final androidx.compose.material.RadioButtonDefaults INSTANCE;
}
public final class RadioButtonKt {
@@ -525,8 +619,14 @@
public final class ShapesKt {
}
- public final class SliderConstants {
- field public static final androidx.compose.material.SliderConstants INSTANCE;
+ @Deprecated public final class SliderConstants {
+ field @Deprecated public static final androidx.compose.material.SliderConstants INSTANCE;
+ field @Deprecated public static final float InactiveTrackColorAlpha = 0.24f;
+ field @Deprecated public static final float TickColorAlpha = 0.54f;
+ }
+
+ public final class SliderDefaults {
+ field public static final androidx.compose.material.SliderDefaults INSTANCE;
field public static final float InactiveTrackColorAlpha = 0.24f;
field public static final float TickColorAlpha = 0.54f;
}
@@ -535,12 +635,12 @@
method @androidx.compose.runtime.Composable public static void Slider-B7rb6FQ(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional @IntRange(from=0) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit> onValueChangeEnd, optional androidx.compose.foundation.InteractionState interactionState, optional long thumbColor, optional long activeTrackColor, optional long inactiveTrackColor, optional long activeTickColor, optional long inactiveTickColor);
}
- public final class SnackbarConstants {
- method public long getDefaultActionPrimaryColor-0d7_KjU();
- method public long getDefaultBackgroundColor-0d7_KjU();
- property public final long defaultActionPrimaryColor;
- property public final long defaultBackgroundColor;
- field public static final androidx.compose.material.SnackbarConstants INSTANCE;
+ @Deprecated public final class SnackbarConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public long getDefaultActionPrimaryColor-0d7_KjU();
+ method @Deprecated @androidx.compose.runtime.Composable public long getDefaultBackgroundColor-0d7_KjU();
+ property @androidx.compose.runtime.Composable public final long defaultActionPrimaryColor;
+ property @androidx.compose.runtime.Composable public final long defaultBackgroundColor;
+ field @Deprecated public static final androidx.compose.material.SnackbarConstants INSTANCE;
}
@androidx.compose.material.ExperimentalMaterialApi public interface SnackbarData {
@@ -554,6 +654,14 @@
property public abstract String message;
}
+ public final class SnackbarDefaults {
+ method @androidx.compose.runtime.Composable public long getBackgroundColor-0d7_KjU();
+ method @androidx.compose.runtime.Composable public long getPrimaryActionColor-0d7_KjU();
+ property @androidx.compose.runtime.Composable public final long backgroundColor;
+ property @androidx.compose.runtime.Composable public final long primaryActionColor;
+ field public static final androidx.compose.material.SnackbarDefaults INSTANCE;
+ }
+
public enum SnackbarDuration {
enum_constant public static final androidx.compose.material.SnackbarDuration Indefinite;
enum_constant public static final androidx.compose.material.SnackbarDuration Long;
@@ -605,13 +713,24 @@
method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.DismissState rememberDismissState(optional androidx.compose.material.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DismissValue,java.lang.Boolean> confirmStateChange);
}
- public final class SwipeableConstants {
- method public androidx.compose.material.ResistanceConfig? defaultResistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
- method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultAnimationSpec();
- method public float getDefaultVelocityThreshold-D9Ej5fM();
+ @Deprecated public final class SwipeableConstants {
+ method @Deprecated public androidx.compose.material.ResistanceConfig? defaultResistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
+ method @Deprecated public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultAnimationSpec();
+ method @Deprecated public float getDefaultVelocityThreshold-D9Ej5fM();
property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> DefaultAnimationSpec;
property public final float DefaultVelocityThreshold;
- field public static final androidx.compose.material.SwipeableConstants INSTANCE;
+ field @Deprecated public static final androidx.compose.material.SwipeableConstants INSTANCE;
+ field @Deprecated public static final float StandardResistanceFactor = 10.0f;
+ field @Deprecated public static final float StiffResistanceFactor = 20.0f;
+ }
+
+ public final class SwipeableDefaults {
+ method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getAnimationSpec();
+ method public float getVelocityThreshold-D9Ej5fM();
+ method public androidx.compose.material.ResistanceConfig? resistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
+ property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> AnimationSpec;
+ property public final float VelocityThreshold;
+ field public static final androidx.compose.material.SwipeableDefaults INSTANCE;
field public static final float StandardResistanceFactor = 10.0f;
field public static final float StiffResistanceFactor = 20.0f;
}
@@ -631,6 +750,8 @@
method public final T! getTargetValue();
method public final T! getValue();
method public final boolean isAnimationRunning();
+ method public final float performDrag(float delta);
+ method public final void performFling(float velocity, kotlin.jvm.functions.Function0<kotlin.Unit> onEnd);
method @androidx.compose.material.ExperimentalMaterialApi public final void snapTo(T? targetValue);
property public final float direction;
property public final boolean isAnimationRunning;
@@ -651,27 +772,46 @@
method public long trackColor-0d7_KjU(boolean enabled, boolean checked);
}
- public final class SwitchConstants {
- method @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors defaultColors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
- field public static final androidx.compose.material.SwitchConstants INSTANCE;
+ @Deprecated public final class SwitchConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors defaultColors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
+ field @Deprecated public static final androidx.compose.material.SwitchConstants INSTANCE;
+ }
+
+ public final class SwitchDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors colors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
+ field public static final androidx.compose.material.SwitchDefaults INSTANCE;
}
public final class SwitchKt {
method @androidx.compose.runtime.Composable public static void Switch(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.InteractionState interactionState, optional androidx.compose.material.SwitchColors colors);
}
- public final class TabConstants {
- method @androidx.compose.runtime.Composable public void DefaultDivider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
- method @androidx.compose.runtime.Composable public void DefaultIndicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
- method public androidx.compose.ui.Modifier defaultTabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
- method public float getDefaultDividerThickness-D9Ej5fM();
- method public float getDefaultIndicatorHeight-D9Ej5fM();
- method public float getDefaultScrollableTabRowPadding-D9Ej5fM();
+ @Deprecated public final class TabConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public void DefaultDivider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
+ method @Deprecated @androidx.compose.runtime.Composable public void DefaultIndicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+ method @Deprecated public androidx.compose.ui.Modifier defaultTabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
+ method @Deprecated public float getDefaultDividerThickness-D9Ej5fM();
+ method @Deprecated public float getDefaultIndicatorHeight-D9Ej5fM();
+ method @Deprecated public float getDefaultScrollableTabRowPadding-D9Ej5fM();
property public final float DefaultDividerThickness;
property public final float DefaultIndicatorHeight;
property public final float DefaultScrollableTabRowPadding;
- field public static final float DefaultDividerOpacity = 0.12f;
- field public static final androidx.compose.material.TabConstants INSTANCE;
+ field @Deprecated public static final float DefaultDividerOpacity = 0.12f;
+ field @Deprecated public static final androidx.compose.material.TabConstants INSTANCE;
+ }
+
+ public final class TabDefaults {
+ method @androidx.compose.runtime.Composable public void Divider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
+ method @androidx.compose.runtime.Composable public void Indicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+ method public float getDividerThickness-D9Ej5fM();
+ method public float getIndicatorHeight-D9Ej5fM();
+ method public float getScrollableTabRowPadding-D9Ej5fM();
+ method public androidx.compose.ui.Modifier tabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
+ property public final float DividerThickness;
+ property public final float IndicatorHeight;
+ property public final float ScrollableTabRowPadding;
+ field public static final float DividerOpacity = 0.12f;
+ field public static final androidx.compose.material.TabDefaults INSTANCE;
}
public final class TabKt {
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index a589728..866aca3 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -12,18 +12,32 @@
method @androidx.compose.runtime.Composable public static void TopAppBar-ye6PvEY(optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional float elevation, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
}
- public final class BackdropScaffoldConstants {
- method public float getDefaultFrontLayerElevation-D9Ej5fM();
- method public long getDefaultFrontLayerScrimColor-0d7_KjU();
- method public androidx.compose.ui.graphics.Shape getDefaultFrontLayerShape();
- method public float getDefaultHeaderHeight-D9Ej5fM();
- method public float getDefaultPeekHeight-D9Ej5fM();
+ @Deprecated public final class BackdropScaffoldConstants {
+ method @Deprecated public float getDefaultFrontLayerElevation-D9Ej5fM();
+ method @Deprecated @androidx.compose.runtime.Composable public long getDefaultFrontLayerScrimColor-0d7_KjU();
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getDefaultFrontLayerShape();
+ method @Deprecated public float getDefaultHeaderHeight-D9Ej5fM();
+ method @Deprecated public float getDefaultPeekHeight-D9Ej5fM();
property public final float DefaultFrontLayerElevation;
- property public final long DefaultFrontLayerScrimColor;
- property public final androidx.compose.ui.graphics.Shape DefaultFrontLayerShape;
+ property @androidx.compose.runtime.Composable public final long DefaultFrontLayerScrimColor;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape DefaultFrontLayerShape;
property public final float DefaultHeaderHeight;
property public final float DefaultPeekHeight;
- field public static final androidx.compose.material.BackdropScaffoldConstants INSTANCE;
+ field @Deprecated public static final androidx.compose.material.BackdropScaffoldConstants INSTANCE;
+ }
+
+ public final class BackdropScaffoldDefaults {
+ method public float getFrontLayerElevation-D9Ej5fM();
+ method @androidx.compose.runtime.Composable public long getFrontLayerScrimColor-0d7_KjU();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getFrontLayerShape();
+ method public float getHeaderHeight-D9Ej5fM();
+ method public float getPeekHeight-D9Ej5fM();
+ property public final float FrontLayerElevation;
+ property public final float HeaderHeight;
+ property public final float PeekHeight;
+ property @androidx.compose.runtime.Composable public final long frontLayerScrimColor;
+ property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape frontLayerShape;
+ field public static final androidx.compose.material.BackdropScaffoldDefaults INSTANCE;
}
public final class BackdropScaffoldKt {
@@ -82,12 +96,20 @@
method @androidx.compose.runtime.Composable public static void BottomNavigationItem-S1l6qvI(kotlin.jvm.functions.Function0<kotlin.Unit> icon, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> label, optional boolean alwaysShowLabels, optional androidx.compose.foundation.InteractionState interactionState, optional long selectedContentColor, optional long unselectedContentColor);
}
- public final class BottomSheetScaffoldConstants {
- method public float getDefaultSheetElevation-D9Ej5fM();
- method public float getDefaultSheetPeekHeight-D9Ej5fM();
+ @Deprecated public final class BottomSheetScaffoldConstants {
+ method @Deprecated public float getDefaultSheetElevation-D9Ej5fM();
+ method @Deprecated public float getDefaultSheetPeekHeight-D9Ej5fM();
property public final float DefaultSheetElevation;
property public final float DefaultSheetPeekHeight;
- field public static final androidx.compose.material.BottomSheetScaffoldConstants INSTANCE;
+ field @Deprecated public static final androidx.compose.material.BottomSheetScaffoldConstants INSTANCE;
+ }
+
+ public final class BottomSheetScaffoldDefaults {
+ method public float getSheetElevation-D9Ej5fM();
+ method public float getSheetPeekHeight-D9Ej5fM();
+ property public final float SheetElevation;
+ property public final float SheetPeekHeight;
+ field public static final androidx.compose.material.BottomSheetScaffoldDefaults INSTANCE;
}
public final class BottomSheetScaffoldKt {
@@ -131,19 +153,19 @@
method public long contentColor-0d7_KjU(boolean enabled);
}
- public final class ButtonConstants {
- method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultButtonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
- method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation defaultElevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
- method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultOutlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
- method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultTextButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
- method public androidx.compose.foundation.layout.PaddingValues getDefaultContentPadding();
- method public float getDefaultIconSize-D9Ej5fM();
- method public float getDefaultIconSpacing-D9Ej5fM();
- method public float getDefaultMinHeight-D9Ej5fM();
- method public float getDefaultMinWidth-D9Ej5fM();
- method public androidx.compose.foundation.BorderStroke getDefaultOutlinedBorder();
- method public androidx.compose.foundation.layout.PaddingValues getDefaultTextContentPadding();
- method public float getOutlinedBorderSize-D9Ej5fM();
+ @Deprecated public final class ButtonConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultButtonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation defaultElevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultOutlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultTextButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+ method @Deprecated public androidx.compose.foundation.layout.PaddingValues getDefaultContentPadding();
+ method @Deprecated public float getDefaultIconSize-D9Ej5fM();
+ method @Deprecated public float getDefaultIconSpacing-D9Ej5fM();
+ method @Deprecated public float getDefaultMinHeight-D9Ej5fM();
+ method @Deprecated public float getDefaultMinWidth-D9Ej5fM();
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke getDefaultOutlinedBorder();
+ method @Deprecated public androidx.compose.foundation.layout.PaddingValues getDefaultTextContentPadding();
+ method @Deprecated public float getOutlinedBorderSize-D9Ej5fM();
property public final androidx.compose.foundation.layout.PaddingValues DefaultContentPadding;
property public final float DefaultIconSize;
property public final float DefaultIconSpacing;
@@ -151,8 +173,33 @@
property public final float DefaultMinWidth;
property public final androidx.compose.foundation.layout.PaddingValues DefaultTextContentPadding;
property public final float OutlinedBorderSize;
- property public final androidx.compose.foundation.BorderStroke defaultOutlinedBorder;
- field public static final androidx.compose.material.ButtonConstants INSTANCE;
+ property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke defaultOutlinedBorder;
+ field @Deprecated public static final androidx.compose.material.ButtonConstants INSTANCE;
+ field @Deprecated public static final float OutlinedBorderOpacity = 0.12f;
+ }
+
+ public final class ButtonDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors buttonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation elevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
+ method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
+ method public float getIconSize-D9Ej5fM();
+ method public float getIconSpacing-D9Ej5fM();
+ method public float getMinHeight-D9Ej5fM();
+ method public float getMinWidth-D9Ej5fM();
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke getOutlinedBorder();
+ method public float getOutlinedBorderSize-D9Ej5fM();
+ method public androidx.compose.foundation.layout.PaddingValues getTextButtonContentPadding();
+ method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors outlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+ method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors textButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
+ property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
+ property public final float IconSize;
+ property public final float IconSpacing;
+ property public final float MinHeight;
+ property public final float MinWidth;
+ property public final float OutlinedBorderSize;
+ property public final androidx.compose.foundation.layout.PaddingValues TextButtonContentPadding;
+ property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke outlinedBorder;
+ field public static final androidx.compose.material.ButtonDefaults INSTANCE;
field public static final float OutlinedBorderOpacity = 0.12f;
}
@@ -176,9 +223,14 @@
method public long checkmarkColor-0d7_KjU(androidx.compose.ui.state.ToggleableState state);
}
- public final class CheckboxConstants {
- method @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors defaultColors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
- field public static final androidx.compose.material.CheckboxConstants INSTANCE;
+ @Deprecated public final class CheckboxConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors defaultColors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
+ field @Deprecated public static final androidx.compose.material.CheckboxConstants INSTANCE;
+ }
+
+ public final class CheckboxDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors colors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
+ field public static final androidx.compose.material.CheckboxDefaults INSTANCE;
}
public final class CheckboxKt {
@@ -224,12 +276,12 @@
}
public final class ContentAlpha {
- method public float getDisabled();
- method public float getHigh();
- method public float getMedium();
- property public final float disabled;
- property public final float high;
- property public final float medium;
+ method @androidx.compose.runtime.Composable public float getDisabled();
+ method @androidx.compose.runtime.Composable public float getHigh();
+ method @androidx.compose.runtime.Composable public float getMedium();
+ property @androidx.compose.runtime.Composable public final float disabled;
+ property @androidx.compose.runtime.Composable public final float high;
+ property @androidx.compose.runtime.Composable public final float medium;
field public static final androidx.compose.material.ContentAlpha INSTANCE;
}
@@ -270,13 +322,22 @@
method @androidx.compose.runtime.Composable public static void Divider-JRSVyrs(optional androidx.compose.ui.Modifier modifier, optional long color, optional float thickness, optional float startIndent);
}
- public final class DrawerConstants {
- method public float getDefaultElevation-D9Ej5fM();
- method public long getDefaultScrimColor-0d7_KjU();
+ @Deprecated public final class DrawerConstants {
+ method @Deprecated public float getDefaultElevation-D9Ej5fM();
+ method @Deprecated @androidx.compose.runtime.Composable public long getDefaultScrimColor-0d7_KjU();
property public final float DefaultElevation;
- property public final long defaultScrimColor;
- field public static final androidx.compose.material.DrawerConstants INSTANCE;
- field public static final float ScrimDefaultOpacity = 0.32f;
+ property @androidx.compose.runtime.Composable public final long defaultScrimColor;
+ field @Deprecated public static final androidx.compose.material.DrawerConstants INSTANCE;
+ field @Deprecated public static final float ScrimDefaultOpacity = 0.32f;
+ }
+
+ public final class DrawerDefaults {
+ method public float getElevation-D9Ej5fM();
+ method @androidx.compose.runtime.Composable public long getScrimColor-0d7_KjU();
+ property public final float Elevation;
+ property @androidx.compose.runtime.Composable public final long scrimColor;
+ field public static final androidx.compose.material.DrawerDefaults INSTANCE;
+ field public static final float ScrimOpacity = 0.32f;
}
public final class DrawerKt {
@@ -306,10 +367,16 @@
enum_constant public static final androidx.compose.material.DrawerValue Open;
}
- public final class ElevationConstants {
+ @Deprecated public final class ElevationConstants {
+ method @Deprecated public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? incomingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
+ method @Deprecated public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? outgoingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
+ field @Deprecated public static final androidx.compose.material.ElevationConstants INSTANCE;
+ }
+
+ public final class ElevationDefaults {
method public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? incomingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
method public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? outgoingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
- field public static final androidx.compose.material.ElevationConstants INSTANCE;
+ field public static final androidx.compose.material.ElevationDefaults INSTANCE;
}
public final class ElevationKt {
@@ -335,12 +402,12 @@
}
@Deprecated public interface EmphasisLevels {
- method @Deprecated public androidx.compose.material.Emphasis getDisabled();
- method @Deprecated public androidx.compose.material.Emphasis getHigh();
- method @Deprecated public androidx.compose.material.Emphasis getMedium();
- property public abstract androidx.compose.material.Emphasis disabled;
- property public abstract androidx.compose.material.Emphasis high;
- property public abstract androidx.compose.material.Emphasis medium;
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getDisabled();
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getHigh();
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getMedium();
+ property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis disabled;
+ property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis high;
+ property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis medium;
}
@kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalMaterialApi {
@@ -356,9 +423,14 @@
method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public androidx.compose.material.FixedThreshold copy-0680j_4(float offset);
}
- public final class FloatingActionButtonConstants {
- method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation defaultElevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
- field public static final androidx.compose.material.FloatingActionButtonConstants INSTANCE;
+ @Deprecated public final class FloatingActionButtonConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation defaultElevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
+ field @Deprecated public static final androidx.compose.material.FloatingActionButtonConstants INSTANCE;
+ }
+
+ public final class FloatingActionButtonDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation elevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
+ field public static final androidx.compose.material.FloatingActionButtonDefaults INSTANCE;
}
@androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface FloatingActionButtonElevation {
@@ -395,12 +467,12 @@
}
public final class MaterialTheme {
- method public androidx.compose.material.Colors getColors();
- method public androidx.compose.material.Shapes getShapes();
- method public androidx.compose.material.Typography getTypography();
- property public final androidx.compose.material.Colors colors;
- property public final androidx.compose.material.Shapes shapes;
- property public final androidx.compose.material.Typography typography;
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public androidx.compose.material.Colors getColors();
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public androidx.compose.material.Shapes getShapes();
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public androidx.compose.material.Typography getTypography();
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final androidx.compose.material.Colors colors;
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final androidx.compose.material.Shapes shapes;
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final androidx.compose.material.Typography typography;
field public static final androidx.compose.material.MaterialTheme INSTANCE;
}
@@ -413,12 +485,20 @@
method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.InteractionState interactionState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
- public final class ModalBottomSheetConstants {
- method public float getDefaultElevation-D9Ej5fM();
- method public long getDefaultScrimColor-0d7_KjU();
+ @Deprecated public final class ModalBottomSheetConstants {
+ method @Deprecated public float getDefaultElevation-D9Ej5fM();
+ method @Deprecated @androidx.compose.runtime.Composable public long getDefaultScrimColor-0d7_KjU();
property public final float DefaultElevation;
- property public final long DefaultScrimColor;
- field public static final androidx.compose.material.ModalBottomSheetConstants INSTANCE;
+ property @androidx.compose.runtime.Composable public final long DefaultScrimColor;
+ field @Deprecated public static final androidx.compose.material.ModalBottomSheetConstants INSTANCE;
+ }
+
+ public final class ModalBottomSheetDefaults {
+ method public float getElevation-D9Ej5fM();
+ method @androidx.compose.runtime.Composable public long getScrimColor-0d7_KjU();
+ property public final float Elevation;
+ property @androidx.compose.runtime.Composable public final long scrimColor;
+ field public static final androidx.compose.material.ModalBottomSheetDefaults INSTANCE;
}
public final class ModalBottomSheetKt {
@@ -450,13 +530,22 @@
method @androidx.compose.runtime.Composable public static void OutlinedTextField-IIju55g(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 androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isErrorValue, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.text.input.ImeAction,? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onImeActionPerformed, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long activeColor, optional long inactiveColor, optional long errorColor);
}
- public final class ProgressIndicatorConstants {
- method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultProgressAnimationSpec();
- method public float getDefaultStrokeWidth-D9Ej5fM();
+ @Deprecated public final class ProgressIndicatorConstants {
+ method @Deprecated public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultProgressAnimationSpec();
+ method @Deprecated public float getDefaultStrokeWidth-D9Ej5fM();
property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> DefaultProgressAnimationSpec;
property public final float DefaultStrokeWidth;
- field public static final float DefaultIndicatorBackgroundOpacity = 0.24f;
- field public static final androidx.compose.material.ProgressIndicatorConstants INSTANCE;
+ field @Deprecated public static final float DefaultIndicatorBackgroundOpacity = 0.24f;
+ field @Deprecated public static final androidx.compose.material.ProgressIndicatorConstants INSTANCE;
+ }
+
+ public final class ProgressIndicatorDefaults {
+ method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getProgressAnimationSpec();
+ method public float getStrokeWidth-D9Ej5fM();
+ property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> ProgressAnimationSpec;
+ property public final float StrokeWidth;
+ field public static final androidx.compose.material.ProgressIndicatorDefaults INSTANCE;
+ field public static final float IndicatorBackgroundOpacity = 0.24f;
}
public final class ProgressIndicatorKt {
@@ -470,9 +559,14 @@
method public long radioColor-0d7_KjU(boolean enabled, boolean selected);
}
- public final class RadioButtonConstants {
- method @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors defaultColors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
- field public static final androidx.compose.material.RadioButtonConstants INSTANCE;
+ @Deprecated public final class RadioButtonConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors defaultColors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
+ field @Deprecated public static final androidx.compose.material.RadioButtonConstants INSTANCE;
+ }
+
+ public final class RadioButtonDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors colors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
+ field public static final androidx.compose.material.RadioButtonDefaults INSTANCE;
}
public final class RadioButtonKt {
@@ -525,8 +619,14 @@
public final class ShapesKt {
}
- public final class SliderConstants {
- field public static final androidx.compose.material.SliderConstants INSTANCE;
+ @Deprecated public final class SliderConstants {
+ field @Deprecated public static final androidx.compose.material.SliderConstants INSTANCE;
+ field @Deprecated public static final float InactiveTrackColorAlpha = 0.24f;
+ field @Deprecated public static final float TickColorAlpha = 0.54f;
+ }
+
+ public final class SliderDefaults {
+ field public static final androidx.compose.material.SliderDefaults INSTANCE;
field public static final float InactiveTrackColorAlpha = 0.24f;
field public static final float TickColorAlpha = 0.54f;
}
@@ -535,12 +635,12 @@
method @androidx.compose.runtime.Composable public static void Slider-B7rb6FQ(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional @IntRange(from=0) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit> onValueChangeEnd, optional androidx.compose.foundation.InteractionState interactionState, optional long thumbColor, optional long activeTrackColor, optional long inactiveTrackColor, optional long activeTickColor, optional long inactiveTickColor);
}
- public final class SnackbarConstants {
- method public long getDefaultActionPrimaryColor-0d7_KjU();
- method public long getDefaultBackgroundColor-0d7_KjU();
- property public final long defaultActionPrimaryColor;
- property public final long defaultBackgroundColor;
- field public static final androidx.compose.material.SnackbarConstants INSTANCE;
+ @Deprecated public final class SnackbarConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public long getDefaultActionPrimaryColor-0d7_KjU();
+ method @Deprecated @androidx.compose.runtime.Composable public long getDefaultBackgroundColor-0d7_KjU();
+ property @androidx.compose.runtime.Composable public final long defaultActionPrimaryColor;
+ property @androidx.compose.runtime.Composable public final long defaultBackgroundColor;
+ field @Deprecated public static final androidx.compose.material.SnackbarConstants INSTANCE;
}
@androidx.compose.material.ExperimentalMaterialApi public interface SnackbarData {
@@ -554,6 +654,14 @@
property public abstract String message;
}
+ public final class SnackbarDefaults {
+ method @androidx.compose.runtime.Composable public long getBackgroundColor-0d7_KjU();
+ method @androidx.compose.runtime.Composable public long getPrimaryActionColor-0d7_KjU();
+ property @androidx.compose.runtime.Composable public final long backgroundColor;
+ property @androidx.compose.runtime.Composable public final long primaryActionColor;
+ field public static final androidx.compose.material.SnackbarDefaults INSTANCE;
+ }
+
public enum SnackbarDuration {
enum_constant public static final androidx.compose.material.SnackbarDuration Indefinite;
enum_constant public static final androidx.compose.material.SnackbarDuration Long;
@@ -605,13 +713,24 @@
method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.DismissState rememberDismissState(optional androidx.compose.material.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DismissValue,java.lang.Boolean> confirmStateChange);
}
- public final class SwipeableConstants {
- method public androidx.compose.material.ResistanceConfig? defaultResistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
- method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultAnimationSpec();
- method public float getDefaultVelocityThreshold-D9Ej5fM();
+ @Deprecated public final class SwipeableConstants {
+ method @Deprecated public androidx.compose.material.ResistanceConfig? defaultResistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
+ method @Deprecated public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultAnimationSpec();
+ method @Deprecated public float getDefaultVelocityThreshold-D9Ej5fM();
property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> DefaultAnimationSpec;
property public final float DefaultVelocityThreshold;
- field public static final androidx.compose.material.SwipeableConstants INSTANCE;
+ field @Deprecated public static final androidx.compose.material.SwipeableConstants INSTANCE;
+ field @Deprecated public static final float StandardResistanceFactor = 10.0f;
+ field @Deprecated public static final float StiffResistanceFactor = 20.0f;
+ }
+
+ public final class SwipeableDefaults {
+ method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getAnimationSpec();
+ method public float getVelocityThreshold-D9Ej5fM();
+ method public androidx.compose.material.ResistanceConfig? resistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
+ property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> AnimationSpec;
+ property public final float VelocityThreshold;
+ field public static final androidx.compose.material.SwipeableDefaults INSTANCE;
field public static final float StandardResistanceFactor = 10.0f;
field public static final float StiffResistanceFactor = 20.0f;
}
@@ -631,6 +750,8 @@
method public final T! getTargetValue();
method public final T! getValue();
method public final boolean isAnimationRunning();
+ method public final float performDrag(float delta);
+ method public final void performFling(float velocity, kotlin.jvm.functions.Function0<kotlin.Unit> onEnd);
method @androidx.compose.material.ExperimentalMaterialApi public final void snapTo(T? targetValue);
property public final float direction;
property public final boolean isAnimationRunning;
@@ -651,27 +772,46 @@
method public long trackColor-0d7_KjU(boolean enabled, boolean checked);
}
- public final class SwitchConstants {
- method @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors defaultColors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
- field public static final androidx.compose.material.SwitchConstants INSTANCE;
+ @Deprecated public final class SwitchConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors defaultColors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
+ field @Deprecated public static final androidx.compose.material.SwitchConstants INSTANCE;
+ }
+
+ public final class SwitchDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors colors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
+ field public static final androidx.compose.material.SwitchDefaults INSTANCE;
}
public final class SwitchKt {
method @androidx.compose.runtime.Composable public static void Switch(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.InteractionState interactionState, optional androidx.compose.material.SwitchColors colors);
}
- public final class TabConstants {
- method @androidx.compose.runtime.Composable public void DefaultDivider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
- method @androidx.compose.runtime.Composable public void DefaultIndicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
- method public androidx.compose.ui.Modifier defaultTabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
- method public float getDefaultDividerThickness-D9Ej5fM();
- method public float getDefaultIndicatorHeight-D9Ej5fM();
- method public float getDefaultScrollableTabRowPadding-D9Ej5fM();
+ @Deprecated public final class TabConstants {
+ method @Deprecated @androidx.compose.runtime.Composable public void DefaultDivider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
+ method @Deprecated @androidx.compose.runtime.Composable public void DefaultIndicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+ method @Deprecated public androidx.compose.ui.Modifier defaultTabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
+ method @Deprecated public float getDefaultDividerThickness-D9Ej5fM();
+ method @Deprecated public float getDefaultIndicatorHeight-D9Ej5fM();
+ method @Deprecated public float getDefaultScrollableTabRowPadding-D9Ej5fM();
property public final float DefaultDividerThickness;
property public final float DefaultIndicatorHeight;
property public final float DefaultScrollableTabRowPadding;
- field public static final float DefaultDividerOpacity = 0.12f;
- field public static final androidx.compose.material.TabConstants INSTANCE;
+ field @Deprecated public static final float DefaultDividerOpacity = 0.12f;
+ field @Deprecated public static final androidx.compose.material.TabConstants INSTANCE;
+ }
+
+ public final class TabDefaults {
+ method @androidx.compose.runtime.Composable public void Divider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
+ method @androidx.compose.runtime.Composable public void Indicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
+ method public float getDividerThickness-D9Ej5fM();
+ method public float getIndicatorHeight-D9Ej5fM();
+ method public float getScrollableTabRowPadding-D9Ej5fM();
+ method public androidx.compose.ui.Modifier tabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
+ property public final float DividerThickness;
+ property public final float IndicatorHeight;
+ property public final float ScrollableTabRowPadding;
+ field public static final float DividerOpacity = 0.12f;
+ field public static final androidx.compose.material.TabDefaults INSTANCE;
}
public final class TabKt {
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ButtonDemo.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ButtonDemo.kt
index db79704..cf2c613 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ButtonDemo.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/ButtonDemo.kt
@@ -28,7 +28,7 @@
import androidx.compose.foundation.layout.preferredSize
import androidx.compose.foundation.shape.GenericShape
import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants
+import androidx.compose.material.ButtonDefaults
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton
import androidx.compose.material.Text
@@ -78,7 +78,7 @@
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
Button(
onClick = {},
- colors = ButtonConstants.defaultButtonColors(
+ colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.secondary
)
) {
@@ -135,7 +135,7 @@
onClick = {},
modifier = Modifier.preferredSize(110.dp),
shape = TriangleShape,
- colors = ButtonConstants.defaultOutlinedButtonColors(
+ colors = ButtonDefaults.outlinedButtonColors(
backgroundColor = Color.Yellow
),
border = BorderStroke(width = 2.dp, color = Color.Black)
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/DynamicThemeActivity.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/DynamicThemeActivity.kt
index 3b8a880..834ffbc5 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/DynamicThemeActivity.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/DynamicThemeActivity.kt
@@ -41,7 +41,6 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -88,9 +87,7 @@
private fun DynamicThemeApp(scrollFraction: ScrollFraction, palette: Colors) {
MaterialTheme(palette) {
val scrollState = rememberScrollState()
- val fraction =
- round((scrollState.value / scrollState.maxValue) * 100) / 100
- remember(fraction) { scrollFraction.value = fraction }
+ scrollFraction.value = round((scrollState.value / scrollState.maxValue) * 100) / 100
Scaffold(
topBar = { TopAppBar({ Text("Scroll down!") }) },
bottomBar = { BottomAppBar(cutoutShape = CircleShape) {} },
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MenuDemo.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MenuDemo.kt
index ddbfc47..0dcc59c 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MenuDemo.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MenuDemo.kt
@@ -85,7 +85,7 @@
expanded = expanded,
onDismissRequest = { expanded = false },
toggle = iconButton,
- dropdownOffset = Position(-12.dp, -12.dp),
+ dropdownOffset = Position(24.dp, 0.dp),
toggleModifier = modifier
) {
options.forEach {
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/TabDemo.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/TabDemo.kt
index 1d503d0..cb37e1c 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/TabDemo.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/TabDemo.kt
@@ -21,7 +21,7 @@
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.preferredHeight
import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants
+import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.material.samples.FancyIndicatorContainerTabs
import androidx.compose.material.samples.FancyIndicatorTabs
@@ -69,7 +69,7 @@
onClick = {
showingSimple.value = !showingSimple.value
},
- colors = ButtonConstants.defaultButtonColors(backgroundColor = Color.Cyan)
+ colors = ButtonDefaults.buttonColors(backgroundColor = Color.Cyan)
) {
Text(buttonText)
}
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BackdropScaffoldSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BackdropScaffoldSamples.kt
index ecbfef5..0294cad 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BackdropScaffoldSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/BackdropScaffoldSamples.kt
@@ -18,9 +18,7 @@
import androidx.annotation.Sampled
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.lazy.LazyColumnFor
+import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.BackdropScaffold
import androidx.compose.material.BackdropValue
import androidx.compose.material.ExperimentalMaterialApi
@@ -40,7 +38,6 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.unit.dp
@@ -88,22 +85,27 @@
)
},
backLayerContent = {
- LazyColumnFor((1..5).toList()) {
- ListItem(
- Modifier.clickable {
- selection.value = it
- scaffoldState.conceal()
- },
- text = { Text("Select $it") }
- )
+ LazyColumn {
+ for (i in 1..5) item {
+ ListItem(
+ Modifier.clickable {
+ selection.value = i
+ scaffoldState.conceal()
+ },
+ text = { Text("Select $i") }
+ )
+ }
}
},
frontLayerContent = {
- Box(
- Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ) {
- Text("Selection: ${selection.value}")
+ Text("Selection: ${selection.value}")
+ LazyColumn {
+ for (i in 1..50) item {
+ ListItem(
+ text = { Text("Item $i") },
+ icon = { Icon(Icons.Default.Favorite) }
+ )
+ }
}
}
)
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ButtonSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ButtonSamples.kt
index e2ac96e..12cfe41 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ButtonSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ButtonSamples.kt
@@ -20,7 +20,7 @@
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants
+import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.OutlinedButton
import androidx.compose.material.Text
@@ -58,8 +58,8 @@
@Composable
fun ButtonWithIconSample() {
Button(onClick = { /* Do something! */ }) {
- Icon(Icons.Filled.Favorite, Modifier.size(ButtonConstants.DefaultIconSize))
- Spacer(Modifier.size(ButtonConstants.DefaultIconSpacing))
+ Icon(Icons.Filled.Favorite, Modifier.size(ButtonDefaults.IconSize))
+ Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text("Like")
}
}
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ModalBottomSheetSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ModalBottomSheetSamples.kt
index 6f72f97..7261692 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ModalBottomSheetSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ModalBottomSheetSamples.kt
@@ -22,6 +22,7 @@
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.preferredHeight
+import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Button
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
@@ -45,11 +46,13 @@
ModalBottomSheetLayout(
sheetState = state,
sheetContent = {
- for (i in 1..5) {
- ListItem(
- text = { Text("Item $i") },
- icon = { Icon(Icons.Default.Favorite) }
- )
+ LazyColumn {
+ for (i in 1..50) item {
+ ListItem(
+ text = { Text("Item $i") },
+ icon = { Icon(Icons.Default.Favorite) }
+ )
+ }
}
}
) {
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ProgressIndicatorSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ProgressIndicatorSamples.kt
index 8c81625..ab3277e 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ProgressIndicatorSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ProgressIndicatorSamples.kt
@@ -24,7 +24,7 @@
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.OutlinedButton
-import androidx.compose.material.ProgressIndicatorConstants
+import androidx.compose.material.ProgressIndicatorDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -41,7 +41,7 @@
var progress by remember { mutableStateOf(0.1f) }
val animatedProgress = animate(
target = progress,
- animSpec = ProgressIndicatorConstants.DefaultProgressAnimationSpec
+ animSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
@@ -63,7 +63,7 @@
var progress by remember { mutableStateOf(0.1f) }
val animatedProgress = animate(
target = progress,
- animSpec = ProgressIndicatorConstants.DefaultProgressAnimationSpec
+ animSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SelectionControlsSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SelectionControlsSamples.kt
index 84b16d4..67c5f45 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SelectionControlsSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SelectionControlsSamples.kt
@@ -25,7 +25,7 @@
import androidx.compose.foundation.layout.preferredHeight
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.Checkbox
-import androidx.compose.material.CheckboxConstants
+import androidx.compose.material.CheckboxDefaults
import androidx.compose.material.MaterialTheme
import androidx.compose.material.RadioButton
import androidx.compose.material.Switch
@@ -66,7 +66,7 @@
TriStateCheckbox(
state = parentState,
onClick = onParentClick,
- colors = CheckboxConstants.defaultColors(
+ colors = CheckboxDefaults.colors(
checkedColor = MaterialTheme.colors.primary
)
)
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SwipeToDismissSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SwipeToDismissSamples.kt
index cfa1849..ca1b39d 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SwipeToDismissSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/SwipeToDismissSamples.kt
@@ -22,7 +22,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumnFor
+import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Card
import androidx.compose.material.DismissDirection.EndToStart
import androidx.compose.material.DismissDirection.StartToEnd
@@ -78,57 +78,63 @@
// will animate to red if you're swiping left or green if you're swiping right. When you let
// go, the item will animate out of the way if you're swiping left (like deleting an email) or
// back to its default position if you're swiping right (like marking an email as read/unread).
- LazyColumnFor(items) { item ->
- var unread by remember { mutableStateOf(false) }
- val dismissState = rememberDismissState(
- confirmStateChange = {
- if (it == DismissedToEnd) unread = !unread
- it != DismissedToEnd
- }
- )
- SwipeToDismiss(
- state = dismissState,
- modifier = Modifier.padding(vertical = 4.dp),
- directions = setOf(StartToEnd, EndToStart),
- dismissThresholds = { direction ->
- FractionalThreshold(if (direction == StartToEnd) 0.25f else 0.5f)
- },
- background = {
- val direction = dismissState.dismissDirection ?: return@SwipeToDismiss
- val color = animate(
- when (dismissState.targetValue) {
- Default -> Color.LightGray
- DismissedToEnd -> Color.Green
- DismissedToStart -> Color.Red
- }
- )
- val alignment = when (direction) {
- StartToEnd -> Alignment.CenterStart
- EndToStart -> Alignment.CenterEnd
+ LazyColumn {
+ items(items) { item ->
+ var unread by remember { mutableStateOf(false) }
+ val dismissState = rememberDismissState(
+ confirmStateChange = {
+ if (it == DismissedToEnd) unread = !unread
+ it != DismissedToEnd
}
- val icon = when (direction) {
- StartToEnd -> Icons.Default.Done
- EndToStart -> Icons.Default.Delete
- }
- val scale = animate(if (dismissState.targetValue == Default) 0.75f else 1f)
-
- Box(
- modifier = Modifier.fillMaxSize().background(color).padding(horizontal = 20.dp),
- contentAlignment = alignment
- ) {
- Icon(icon, Modifier.scale(scale))
- }
- },
- dismissContent = {
- Card(
- elevation = animate(if (dismissState.dismissDirection != null) 4.dp else 0.dp)
- ) {
- ListItem(
- text = { Text(item, fontWeight = if (unread) FontWeight.Bold else null) },
- secondaryText = { Text("Swipe me left or right!") }
+ )
+ SwipeToDismiss(
+ state = dismissState,
+ modifier = Modifier.padding(vertical = 4.dp),
+ directions = setOf(StartToEnd, EndToStart),
+ dismissThresholds = { direction ->
+ FractionalThreshold(if (direction == StartToEnd) 0.25f else 0.5f)
+ },
+ background = {
+ val direction = dismissState.dismissDirection ?: return@SwipeToDismiss
+ val color = animate(
+ when (dismissState.targetValue) {
+ Default -> Color.LightGray
+ DismissedToEnd -> Color.Green
+ DismissedToStart -> Color.Red
+ }
)
+ val alignment = when (direction) {
+ StartToEnd -> Alignment.CenterStart
+ EndToStart -> Alignment.CenterEnd
+ }
+ val icon = when (direction) {
+ StartToEnd -> Icons.Default.Done
+ EndToStart -> Icons.Default.Delete
+ }
+ val scale = animate(if (dismissState.targetValue == Default) 0.75f else 1f)
+
+ Box(
+ Modifier.fillMaxSize().background(color).padding(horizontal = 20.dp),
+ contentAlignment = alignment
+ ) {
+ Icon(icon, Modifier.scale(scale))
+ }
+ },
+ dismissContent = {
+ Card(
+ elevation = animate(
+ if (dismissState.dismissDirection != null) 4.dp else 0.dp
+ )
+ ) {
+ ListItem(
+ text = {
+ Text(item, fontWeight = if (unread) FontWeight.Bold else null)
+ },
+ secondaryText = { Text("Swipe me left or right!") }
+ )
+ }
}
- }
- )
+ )
+ }
}
}
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TabSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TabSamples.kt
index b76d5a8..a6b9745 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TabSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TabSamples.kt
@@ -41,7 +41,7 @@
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ScrollableTabRow
import androidx.compose.material.Tab
-import androidx.compose.material.TabConstants.defaultTabIndicatorOffset
+import androidx.compose.material.TabDefaults.tabIndicatorOffset
import androidx.compose.material.TabPosition
import androidx.compose.material.TabRow
import androidx.compose.material.Text
@@ -189,7 +189,7 @@
// Reuse the default offset animation modifier, but use our own indicator
val indicator = @Composable { tabPositions: List<TabPosition> ->
- FancyIndicator(Color.White, Modifier.defaultTabIndicatorOffset(tabPositions[state]))
+ FancyIndicator(Color.White, Modifier.tabIndicatorOffset(tabPositions[state]))
}
Column {
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonTest.kt
index 03c84ef..452942f 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonTest.kt
@@ -395,7 +395,7 @@
Button(
onClick = {},
enabled = false,
- colors = ButtonConstants.defaultButtonColors(
+ colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Red
),
shape = RectangleShape
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt
index f81b3d9..9f831e1 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt
@@ -182,7 +182,7 @@
FloatingActionButton(
modifier = Modifier.testTag("myButton"),
onClick = {},
- elevation = FloatingActionButtonConstants.defaultElevation(
+ elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 0.dp
)
) {
@@ -219,7 +219,7 @@
ExtendedFloatingActionButton(
modifier = Modifier.testTag("myButton"),
onClick = {},
- elevation = FloatingActionButtonConstants.defaultElevation(
+ elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 0.dp
),
text = { Box(Modifier.preferredSize(10.dp, 50.dp)) }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
index 64e7730..aff45d5 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
@@ -144,7 +144,7 @@
)
assertThat(ltrPosition.x).isEqualTo(
- anchorPosition.x + anchorSize.width + offsetX
+ anchorPosition.x + offsetX
)
assertThat(ltrPosition.y).isEqualTo(
anchorPosition.y + anchorSize.height + offsetY
@@ -161,7 +161,7 @@
)
assertThat(rtlPosition.x).isEqualTo(
- anchorPosition.x - popupSize.width - offsetX
+ anchorPosition.x + anchorSize.width - offsetX - popupSize.width
)
assertThat(rtlPosition.y).isEqualTo(
anchorPosition.y + anchorSize.height + offsetY
@@ -192,7 +192,7 @@
)
assertThat(ltrPosition.x).isEqualTo(
- anchorPosition.x - popupSize.width - offsetX
+ anchorPosition.x + anchorSize.width - offsetX - popupSize.width
)
assertThat(ltrPosition.y).isEqualTo(
anchorPosition.y - popupSize.height - offsetY
@@ -209,7 +209,7 @@
)
assertThat(rtlPosition.x).isEqualTo(
- anchorPositionRtl.x + anchorSize.width + offsetX
+ anchorPositionRtl.x + offsetX
)
assertThat(rtlPosition.y).isEqualTo(
anchorPositionRtl.y - popupSize.height - offsetY
@@ -275,9 +275,9 @@
assertThat(obtainedParentBounds).isEqualTo(IntBounds(anchorPosition, anchorSize))
assertThat(obtainedMenuBounds).isEqualTo(
IntBounds(
- anchorPosition.x + anchorSize.width + offsetX,
+ anchorPosition.x + offsetX,
anchorPosition.y + anchorSize.height + offsetY,
- anchorPosition.x + anchorSize.width + offsetX + popupSize.width,
+ anchorPosition.x + offsetX + popupSize.width,
anchorPosition.y + anchorSize.height + offsetY + popupSize.height
)
)
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarHostTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarHostTest.kt
index 915e930..ca5dee3 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarHostTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SnackbarHostTest.kt
@@ -16,8 +16,8 @@
package androidx.compose.material
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
@@ -52,7 +52,7 @@
rule.setContent {
scope = rememberCoroutineScope()
SnackbarHost(hostState) { data ->
- remember(data) {
+ LaunchedEffect(data) {
resultedInvocation += data.message
data.dismiss()
}
@@ -79,9 +79,9 @@
rule.setContent {
scope = rememberCoroutineScope()
SnackbarHost(hostState) { data ->
- remember(data) {
+ LaunchedEffect(data) {
resultedInvocation += data.message
- scope.launch {
+ launch {
delay(30L)
data.dismiss()
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwipeableTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwipeableTest.kt
index ba151db..21e4fa0 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwipeableTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwipeableTest.kt
@@ -18,27 +18,41 @@
import androidx.compose.animation.core.AnimationEndReason
import androidx.compose.animation.core.ManualAnimationClock
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.ScrollableColumn
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.preferredSize
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.gesture.nestedscroll.nestedScroll
import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.center
+import androidx.compose.ui.test.centerX
+import androidx.compose.ui.test.centerY
+import androidx.compose.ui.test.down
import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.moveBy
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performGesture
import androidx.compose.ui.test.swipe
import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.test.up
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.milliseconds
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
@@ -1521,6 +1535,223 @@
}
}
+ @Test
+ fun swipeable_defaultVerticalNestedScrollConnection_nestedDrag() {
+ lateinit var swipeableState: SwipeableState<String>
+ lateinit var anchors: MutableState<Map<Float, String>>
+ lateinit var scrollState: ScrollState
+ rule.setContent {
+ swipeableState = rememberSwipeableState("A")
+ anchors = remember { mutableStateOf(mapOf(0f to "A", -1000f to "B")) }
+ scrollState = rememberScrollState()
+ Box(
+ Modifier
+ .preferredSize(300.dp)
+ .nestedScroll(swipeableState.PreUpPostDownNestedScrollConnection)
+ .swipeable(
+ state = swipeableState,
+ anchors = anchors.value,
+ thresholds = { _, _ -> FractionalThreshold(0.5f) },
+ orientation = Orientation.Horizontal
+ )
+ ) {
+ ScrollableColumn(
+ scrollState = scrollState,
+ modifier = Modifier.fillMaxWidth().testTag(swipeableTag)
+ ) {
+ repeat(100) {
+ Text(text = it.toString(), modifier = Modifier.height(50.dp))
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(swipeableState.value).isEqualTo("A")
+ }
+
+ rule.onNodeWithTag(swipeableTag)
+ .performGesture {
+ down(Offset(x = 10f, y = 10f))
+ moveBy(Offset(x = 0f, y = -1500f))
+ up()
+ }
+ advanceClock()
+
+ rule.runOnIdle {
+ assertThat(swipeableState.value).isEqualTo("B")
+ assertThat(scrollState.value).isGreaterThan(0f)
+ }
+
+ rule.onNodeWithTag(swipeableTag)
+ .performGesture {
+ down(Offset(x = 10f, y = 10f))
+ moveBy(Offset(x = 0f, y = 1500f))
+ up()
+ }
+
+ advanceClock()
+
+ rule.runOnIdle {
+ assertThat(swipeableState.value).isEqualTo("A")
+ assertThat(scrollState.value).isEqualTo(0f)
+ }
+ }
+
+ @Test
+ fun swipeable_nestedScroll_preFling() {
+ lateinit var swipeableState: SwipeableState<String>
+ lateinit var anchors: MutableState<Map<Float, String>>
+ lateinit var scrollState: ScrollState
+ rule.setContent {
+ swipeableState = rememberSwipeableState("A")
+ anchors = remember { mutableStateOf(mapOf(0f to "A", -1000f to "B")) }
+ scrollState = rememberScrollState()
+ Box(
+ Modifier
+ .preferredSize(300.dp)
+ .nestedScroll(swipeableState.PreUpPostDownNestedScrollConnection)
+ .swipeable(
+ state = swipeableState,
+ anchors = anchors.value,
+ thresholds = { _, _ -> FixedThreshold(56.dp) },
+ orientation = Orientation.Horizontal
+ )
+ ) {
+ ScrollableColumn(
+ scrollState = scrollState,
+ modifier = Modifier.fillMaxWidth().testTag(swipeableTag)
+ ) {
+ repeat(100) {
+ Text(text = it.toString(), modifier = Modifier.height(50.dp))
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(swipeableState.value).isEqualTo("A")
+ }
+
+ rule.onNodeWithTag(swipeableTag)
+ .performGesture {
+ swipeWithVelocity(
+ center,
+ center.copy(y = centerY - 500, x = centerX),
+ duration = 50.milliseconds,
+ endVelocity = 20000f
+ )
+ }
+
+ advanceClock()
+
+ rule.runOnIdle {
+ assertThat(swipeableState.value).isEqualTo("B")
+ // should eat all velocity, no internal scroll
+ assertThat(scrollState.value).isEqualTo(0f)
+ }
+
+ rule.onNodeWithTag(swipeableTag)
+ .performGesture {
+ swipeWithVelocity(
+ center,
+ center.copy(y = centerY + 500, x = centerX),
+ duration = 50.milliseconds,
+ endVelocity = 20000f
+ )
+ }
+
+ advanceClock()
+
+ rule.runOnIdle {
+ assertThat(swipeableState.value).isEqualTo("A")
+ assertThat(scrollState.value).isEqualTo(0f)
+ }
+ }
+
+ @Test
+ fun swipeable_nestedScroll_postFlings() {
+ lateinit var swipeableState: SwipeableState<String>
+ lateinit var anchors: MutableState<Map<Float, String>>
+ lateinit var scrollState: ScrollState
+ rule.setContent {
+ swipeableState = rememberSwipeableState("B")
+ anchors = remember { mutableStateOf(mapOf(0f to "A", -1000f to "B")) }
+ scrollState = rememberScrollState(initial = 5000f)
+ Box(
+ Modifier
+ .preferredSize(300.dp)
+ .nestedScroll(swipeableState.PreUpPostDownNestedScrollConnection)
+ .swipeable(
+ state = swipeableState,
+ anchors = anchors.value,
+ thresholds = { _, _ -> FixedThreshold(56.dp) },
+ orientation = Orientation.Horizontal
+ )
+ ) {
+ ScrollableColumn(
+ scrollState = scrollState,
+ modifier = Modifier.fillMaxWidth().testTag(swipeableTag)
+ ) {
+ repeat(100) {
+ Text(text = it.toString(), modifier = Modifier.height(50.dp))
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(swipeableState.value).isEqualTo("B")
+ assertThat(scrollState.value).isEqualTo(5000f)
+ }
+
+ rule.onNodeWithTag(swipeableTag)
+ .performGesture {
+ // swipe less than scrollState.value but with velocity to test that backdrop won't
+ // move when receives, because it's at anchor
+ swipeWithVelocity(
+ center,
+ center.copy(y = centerY + 1500, x = centerX),
+ duration = 50.milliseconds,
+ endVelocity = 20000f
+ )
+ }
+
+ advanceClock()
+
+ rule.runOnIdle {
+ assertThat(swipeableState.value).isEqualTo("B")
+ assertThat(scrollState.value).isEqualTo(0f)
+ // set value again to test overshoot
+ scrollState.scrollBy(500f)
+ }
+
+ advanceClock()
+
+ rule.runOnIdle {
+ assertThat(swipeableState.value).isEqualTo("B")
+ assertThat(scrollState.value).isEqualTo(500f)
+ }
+
+ rule.onNodeWithTag(swipeableTag)
+ .performGesture {
+ // swipe more than scrollState.value so backdrop start receiving nested scroll
+ swipeWithVelocity(
+ center,
+ center.copy(y = centerY + 1500, x = centerX),
+ duration = 50.milliseconds,
+ endVelocity = 20000f
+ )
+ }
+
+ advanceClock()
+
+ rule.runOnIdle {
+ assertThat(swipeableState.value).isEqualTo("A")
+ assertThat(scrollState.value).isEqualTo(0f)
+ }
+ }
+
private fun swipeRight(
offset: Float = 100f,
velocity: Float? = null
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
index 9cf48ed..9b05fd8 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwitchScreenshotTest.kt
@@ -98,7 +98,7 @@
Switch(
checked = true,
onCheckedChange = { },
- colors = SwitchConstants.defaultColors(checkedThumbColor = Color.Red)
+ colors = SwitchDefaults.colors(checkedThumbColor = Color.Red)
)
}
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabTest.kt
index a10d1cb..bd3d988 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabTest.kt
@@ -19,7 +19,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.preferredHeight
-import androidx.compose.material.TabConstants.defaultTabIndicatorOffset
+import androidx.compose.material.TabDefaults.tabIndicatorOffset
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.samples.ScrollingTextTabs
@@ -124,7 +124,7 @@
val indicator = @Composable { tabPositions: List<TabPosition> ->
Box(
Modifier
- .defaultTabIndicatorOffset(tabPositions[state])
+ .tabIndicatorOffset(tabPositions[state])
.fillMaxWidth()
.preferredHeight(indicatorHeight)
.background(color = Color.Red)
@@ -299,7 +299,7 @@
val indicator = @Composable { tabPositions: List<TabPosition> ->
Box(
Modifier
- .defaultTabIndicatorOffset(tabPositions[state])
+ .tabIndicatorOffset(tabPositions[state])
.fillMaxWidth()
.preferredHeight(indicatorHeight)
.background(color = Color.Red)
@@ -330,7 +330,7 @@
rule.onNodeWithTag("indicator")
.assertPositionInRootIsEqualTo(
// Tabs in a scrollable tab row are offset 52.dp from each end
- expectedLeft = TabConstants.DefaultScrollableTabRowPadding,
+ expectedLeft = TabDefaults.ScrollableTabRowPadding,
expectedTop = tabRowBounds.height - indicatorHeight
)
@@ -341,7 +341,7 @@
// should be in the middle of the TabRow
rule.onNodeWithTag("indicator")
.assertPositionInRootIsEqualTo(
- expectedLeft = TabConstants.DefaultScrollableTabRowPadding + minimumTabWidth,
+ expectedLeft = TabDefaults.ScrollableTabRowPadding + minimumTabWidth,
expectedTop = tabRowBounds.height - indicatorHeight
)
}
@@ -446,8 +446,8 @@
fun testInspectorValue() {
val pos = TabPosition(10.0.dp, 200.0.dp)
rule.setContent {
- val modifier = Modifier.defaultTabIndicatorOffset(pos) as InspectableValue
- assertThat(modifier.nameFallback).isEqualTo("defaultTabIndicatorOffset")
+ val modifier = Modifier.tabIndicatorOffset(pos) as InspectableValue
+ assertThat(modifier.nameFallback).isEqualTo("tabIndicatorOffset")
assertThat(modifier.valueOverride).isEqualTo(pos)
assertThat(modifier.inspectableElements.asIterable()).isEmpty()
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
index 80d88be..1b45759 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
@@ -36,7 +36,6 @@
import androidx.compose.runtime.remember
import androidx.compose.testutils.assertShape
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.isFocused
import androidx.compose.ui.focusObserver
import androidx.compose.ui.geometry.Offset
@@ -81,7 +80,7 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalFocus::class, ExperimentalTesting::class)
+@OptIn(ExperimentalTesting::class)
class OutlinedTextFieldTest {
private val ExpectedMinimumTextFieldHeight = 56.dp
private val ExpectedPadding = 16.dp
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
index 8b2d1f3..4590ed3 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
@@ -42,7 +42,6 @@
import androidx.compose.testutils.assertShape
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.isFocused
import androidx.compose.ui.focusObserver
@@ -95,7 +94,7 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalFocus::class, ExperimentalTesting::class)
+@OptIn(ExperimentalTesting::class)
class TextFieldTest {
private val ExpectedMinimumTextFieldHeight = 56.dp
@@ -194,8 +193,7 @@
@Test
fun testTextField_showHideKeyboardBasedOnFocus() {
- val parentFocusRequester = FocusRequester()
- val focusRequester = FocusRequester()
+ val (focusRequester, parentFocusRequester) = FocusRequester.createRefs()
lateinit var hostView: View
rule.setMaterialContent {
hostView = AmbientView.current
@@ -224,8 +222,7 @@
@Test
fun testTextField_clickingOnTextAfterDismissingKeyboard_showsKeyboard() {
- val parentFocusRequester = FocusRequester()
- val focusRequester = FocusRequester()
+ val (focusRequester, parentFocusRequester) = FocusRequester.createRefs()
lateinit var softwareKeyboardController: SoftwareKeyboardController
lateinit var hostView: View
rule.setMaterialContent {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BackdropScaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BackdropScaffold.kt
index 4d4bad92..7048b39 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BackdropScaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BackdropScaffold.kt
@@ -23,8 +23,8 @@
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.TweenSpec
import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
@@ -38,6 +38,7 @@
import androidx.compose.runtime.savedinstancestate.rememberSavedInstanceState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.gesture.nestedscroll.nestedScroll
import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
import androidx.compose.ui.gesture.tapGestureFilter
import androidx.compose.ui.graphics.Color
@@ -86,7 +87,7 @@
class BackdropScaffoldState(
initialValue: BackdropValue,
clock: AnimationClockObservable,
- animationSpec: AnimationSpec<Float> = SwipeableConstants.DefaultAnimationSpec,
+ animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
confirmStateChange: (BackdropValue) -> Boolean = { true },
val snackbarHostState: SnackbarHostState = SnackbarHostState()
) : SwipeableState<BackdropValue>(
@@ -139,6 +140,8 @@
)
}
+ internal val nestedScrollConnection = this.PreUpPostDownNestedScrollConnection
+
companion object {
/**
* The default [Saver] implementation for [BackdropScaffoldState].
@@ -177,7 +180,7 @@
fun rememberBackdropScaffoldState(
initialValue: BackdropValue,
clock: AnimationClockObservable = AmbientAnimationClock.current,
- animationSpec: AnimationSpec<Float> = SwipeableConstants.DefaultAnimationSpec,
+ animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
confirmStateChange: (BackdropValue) -> Boolean = { true },
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
): BackdropScaffoldState {
@@ -268,17 +271,17 @@
modifier: Modifier = Modifier,
scaffoldState: BackdropScaffoldState = rememberBackdropScaffoldState(Concealed),
gesturesEnabled: Boolean = true,
- peekHeight: Dp = BackdropScaffoldConstants.DefaultPeekHeight,
- headerHeight: Dp = BackdropScaffoldConstants.DefaultHeaderHeight,
+ peekHeight: Dp = BackdropScaffoldDefaults.PeekHeight,
+ headerHeight: Dp = BackdropScaffoldDefaults.HeaderHeight,
persistentAppBar: Boolean = true,
stickyFrontLayer: Boolean = true,
backLayerBackgroundColor: Color = MaterialTheme.colors.primary,
backLayerContentColor: Color = contentColorFor(backLayerBackgroundColor),
- frontLayerShape: Shape = BackdropScaffoldConstants.DefaultFrontLayerShape,
- frontLayerElevation: Dp = BackdropScaffoldConstants.DefaultFrontLayerElevation,
+ frontLayerShape: Shape = BackdropScaffoldDefaults.frontLayerShape,
+ frontLayerElevation: Dp = BackdropScaffoldDefaults.FrontLayerElevation,
frontLayerBackgroundColor: Color = MaterialTheme.colors.surface,
frontLayerContentColor: Color = contentColorFor(frontLayerBackgroundColor),
- frontLayerScrimColor: Color = BackdropScaffoldConstants.DefaultFrontLayerScrimColor,
+ frontLayerScrimColor: Color = BackdropScaffoldDefaults.frontLayerScrimColor,
snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
appBar: @Composable () -> Unit,
backLayerContent: @Composable () -> Unit,
@@ -317,15 +320,17 @@
revealedHeight = min(revealedHeight, backLayerHeight)
}
- val swipeable = Modifier.swipeable(
- state = scaffoldState,
- anchors = mapOf(
- peekHeightPx to Concealed,
- revealedHeight to Revealed
- ),
- orientation = Orientation.Vertical,
- enabled = gesturesEnabled
- )
+ val swipeable = Modifier
+ .nestedScroll(scaffoldState.nestedScrollConnection)
+ .swipeable(
+ state = scaffoldState,
+ anchors = mapOf(
+ peekHeightPx to Concealed,
+ revealedHeight to Revealed
+ ),
+ orientation = Orientation.Vertical,
+ enabled = gesturesEnabled
+ )
// Front layer
Surface(
@@ -461,6 +466,13 @@
/**
* Contains useful constants for [BackdropScaffold].
*/
+@Deprecated(
+ "BackdropScaffoldConstants has been replaced with BackdropScaffoldDefaults",
+ ReplaceWith(
+ "BackdropScaffoldDefaults",
+ "androidx.compose.material.BackdropScaffoldDefaults"
+ )
+)
object BackdropScaffoldConstants {
/**
@@ -476,8 +488,8 @@
/**
* The default shape of the front layer.
*/
- @Composable
val DefaultFrontLayerShape: Shape
+ @Composable
get() = MaterialTheme.shapes.large
.copy(topLeft = CornerSize(16.dp), topRight = CornerSize(16.dp))
@@ -489,9 +501,43 @@
/**
* The default color of the scrim applied to the front layer.
*/
- @Composable
val DefaultFrontLayerScrimColor: Color
- get() = MaterialTheme.colors.surface.copy(alpha = 0.60f)
+ @Composable get() = MaterialTheme.colors.surface.copy(alpha = 0.60f)
+}
+
+/**
+ * Contains useful defaults for [BackdropScaffold].
+ */
+object BackdropScaffoldDefaults {
+
+ /**
+ * The default peek height of the back layer.
+ */
+ val PeekHeight = 56.dp
+
+ /**
+ * The default header height of the front layer.
+ */
+ val HeaderHeight = 48.dp
+
+ /**
+ * The default shape of the front layer.
+ */
+ val frontLayerShape: Shape
+ @Composable
+ get() = MaterialTheme.shapes.large
+ .copy(topLeft = CornerSize(16.dp), topRight = CornerSize(16.dp))
+
+ /**
+ * The default elevation of the front layer.
+ */
+ val FrontLayerElevation = 1.dp
+
+ /**
+ * The default color of the scrim applied to the front layer.
+ */
+ val frontLayerScrimColor: Color
+ @Composable get() = MaterialTheme.colors.surface.copy(alpha = 0.60f)
}
private val AnimationSlideOffset = 20.dp
\ No newline at end of file
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
index ba1e9b6..c210065 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
@@ -37,12 +37,13 @@
import androidx.compose.runtime.savedinstancestate.Saver
import androidx.compose.runtime.savedinstancestate.rememberSavedInstanceState
import androidx.compose.runtime.setValue
-import androidx.compose.ui.layout.Layout
import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.WithConstraints
+import androidx.compose.ui.gesture.nestedscroll.nestedScroll
import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.WithConstraints
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.AmbientAnimationClock
import androidx.compose.ui.platform.AmbientDensity
@@ -79,7 +80,7 @@
class BottomSheetState(
initialValue: BottomSheetValue,
clock: AnimationClockObservable,
- animationSpec: AnimationSpec<Float> = SwipeableConstants.DefaultAnimationSpec,
+ animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
confirmStateChange: (BottomSheetValue) -> Boolean = { true }
) : SwipeableState<BottomSheetValue>(
initialValue = initialValue,
@@ -151,6 +152,8 @@
}
)
}
+
+ internal val nestedScrollConnection = this.PreUpPostDownNestedScrollConnection
}
/**
@@ -164,7 +167,7 @@
@ExperimentalMaterialApi
fun rememberBottomSheetState(
initialValue: BottomSheetValue,
- animationSpec: AnimationSpec<Float> = SwipeableConstants.DefaultAnimationSpec,
+ animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
confirmStateChange: (BottomSheetValue) -> Boolean = { true }
): BottomSheetState {
val disposableClock = AmbientAnimationClock.current.asDisposableClock()
@@ -279,17 +282,17 @@
floatingActionButtonPosition: FabPosition = FabPosition.End,
sheetGesturesEnabled: Boolean = true,
sheetShape: Shape = MaterialTheme.shapes.large,
- sheetElevation: Dp = BottomSheetScaffoldConstants.DefaultSheetElevation,
+ sheetElevation: Dp = BottomSheetScaffoldDefaults.SheetElevation,
sheetBackgroundColor: Color = MaterialTheme.colors.surface,
sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
- sheetPeekHeight: Dp = BottomSheetScaffoldConstants.DefaultSheetPeekHeight,
+ sheetPeekHeight: Dp = BottomSheetScaffoldDefaults.SheetPeekHeight,
drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
drawerGesturesEnabled: Boolean = true,
drawerShape: Shape = MaterialTheme.shapes.large,
- drawerElevation: Dp = DrawerConstants.DefaultElevation,
+ drawerElevation: Dp = DrawerDefaults.Elevation,
drawerBackgroundColor: Color = MaterialTheme.colors.surface,
drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
- drawerScrimColor: Color = DrawerConstants.defaultScrimColor,
+ drawerScrimColor: Color = DrawerDefaults.scrimColor,
backgroundColor: Color = MaterialTheme.colors.background,
contentColor: Color = contentColorFor(backgroundColor),
bodyContent: @Composable (PaddingValues) -> Unit
@@ -299,16 +302,18 @@
val peekHeightPx = with(AmbientDensity.current) { sheetPeekHeight.toPx() }
var bottomSheetHeight by remember { mutableStateOf(fullHeight) }
- val swipeable = Modifier.swipeable(
- state = scaffoldState.bottomSheetState,
- anchors = mapOf(
- fullHeight - peekHeightPx to BottomSheetValue.Collapsed,
- fullHeight - bottomSheetHeight to BottomSheetValue.Expanded
- ),
- orientation = Orientation.Vertical,
- enabled = sheetGesturesEnabled,
- resistance = null
- )
+ val swipeable = Modifier
+ .nestedScroll(scaffoldState.bottomSheetState.nestedScrollConnection)
+ .swipeable(
+ state = scaffoldState.bottomSheetState,
+ anchors = mapOf(
+ fullHeight - peekHeightPx to BottomSheetValue.Collapsed,
+ fullHeight - bottomSheetHeight to BottomSheetValue.Expanded
+ ),
+ orientation = Orientation.Vertical,
+ enabled = sheetGesturesEnabled,
+ resistance = null
+ )
val child = @Composable {
BottomSheetScaffoldStack(
@@ -422,6 +427,13 @@
/**
* Contains useful constants for [BottomSheetScaffold].
*/
+@Deprecated(
+ message = "BottomSheetScaffoldConstants has been replaced with BottomSheetScaffoldDefaults",
+ ReplaceWith(
+ "BottomSheetScaffoldDefaults",
+ "androidx.compose.material.BottomSheetScaffoldDefaults"
+ )
+)
object BottomSheetScaffoldConstants {
/**
@@ -433,4 +445,20 @@
* The default peek height used by [BottomSheetScaffold].
*/
val DefaultSheetPeekHeight = 56.dp
+}
+
+/**
+ * Contains useful defaults for [BottomSheetScaffold].
+ */
+object BottomSheetScaffoldDefaults {
+
+ /**
+ * The default elevation used by [BottomSheetScaffold].
+ */
+ val SheetElevation = 8.dp
+
+ /**
+ * The default peek height used by [BottomSheetScaffold].
+ */
+ val SheetPeekHeight = 56.dp
}
\ No newline at end of file
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt
index cdeff5d..b23e6fe 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt
@@ -19,10 +19,10 @@
package androidx.compose.material
import androidx.compose.animation.AnimatedValueModel
-import androidx.compose.animation.VectorConverter
import androidx.compose.animation.asDisposableClock
import androidx.compose.animation.core.AnimationClockObservable
import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.VectorConverter
import androidx.compose.foundation.AmbientIndication
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Interaction
@@ -82,11 +82,11 @@
* it is [Interaction.Pressed].
* @param elevation [ButtonElevation] used to resolve the elevation for this button in different
* states. This controls the size of the shadow below the button. Pass `null` here to disable
- * elevation for this button. See [ButtonConstants.defaultElevation].
+ * elevation for this button. See [ButtonDefaults.elevation].
* @param shape Defines the button's shape as well as its shadow
* @param border Border to draw around the button
* @param colors [ButtonColors] that will be used to resolve the background and content color for
- * this button in different states. See [ButtonConstants.defaultButtonColors].
+ * this button in different states. See [ButtonDefaults.buttonColors].
* @param contentPadding The spacing values to apply internally between the container and the content
*/
@OptIn(ExperimentalMaterialApi::class)
@@ -96,11 +96,11 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionState: InteractionState = remember { InteractionState() },
- elevation: ButtonElevation? = ButtonConstants.defaultElevation(),
+ elevation: ButtonElevation? = ButtonDefaults.elevation(),
shape: Shape = MaterialTheme.shapes.small,
border: BorderStroke? = null,
- colors: ButtonColors = ButtonConstants.defaultButtonColors(),
- contentPadding: PaddingValues = ButtonConstants.DefaultContentPadding,
+ colors: ButtonColors = ButtonDefaults.buttonColors(),
+ contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
content: @Composable RowScope.() -> Unit
) {
// TODO(aelias): Avoid manually putting the clickable above the clip and
@@ -128,8 +128,8 @@
Row(
Modifier
.defaultMinSizeConstraints(
- minWidth = ButtonConstants.DefaultMinWidth,
- minHeight = ButtonConstants.DefaultMinHeight
+ minWidth = ButtonDefaults.MinWidth,
+ minHeight = ButtonDefaults.MinHeight
)
.indication(interactionState, AmbientIndication.current())
.padding(contentPadding),
@@ -176,7 +176,7 @@
* @param shape Defines the button's shape as well as its shadow
* @param border Border to draw around the button
* @param colors [ButtonColors] that will be used to resolve the background and content color for
- * this button in different states. See [ButtonConstants.defaultOutlinedButtonColors].
+ * this button in different states. See [ButtonDefaults.outlinedButtonColors].
* @param contentPadding The spacing values to apply internally between the container and the content
*/
@OptIn(ExperimentalMaterialApi::class)
@@ -188,9 +188,9 @@
interactionState: InteractionState = remember { InteractionState() },
elevation: ButtonElevation? = null,
shape: Shape = MaterialTheme.shapes.small,
- border: BorderStroke? = ButtonConstants.defaultOutlinedBorder,
- colors: ButtonColors = ButtonConstants.defaultOutlinedButtonColors(),
- contentPadding: PaddingValues = ButtonConstants.DefaultContentPadding,
+ border: BorderStroke? = ButtonDefaults.outlinedBorder,
+ colors: ButtonColors = ButtonDefaults.outlinedButtonColors(),
+ contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
noinline content: @Composable RowScope.() -> Unit
) = Button(
onClick = onClick,
@@ -236,7 +236,7 @@
* @param shape Defines the button's shape as well as its shadow
* @param border Border to draw around the button
* @param colors [ButtonColors] that will be used to resolve the background and content color for
- * this button in different states. See [ButtonConstants.defaultTextButtonColors].
+ * this button in different states. See [ButtonDefaults.textButtonColors].
* @param contentPadding The spacing values to apply internally between the container and the content
*/
@OptIn(ExperimentalMaterialApi::class)
@@ -249,8 +249,8 @@
elevation: ButtonElevation? = null,
shape: Shape = MaterialTheme.shapes.small,
border: BorderStroke? = null,
- colors: ButtonColors = ButtonConstants.defaultTextButtonColors(),
- contentPadding: PaddingValues = ButtonConstants.DefaultTextContentPadding,
+ colors: ButtonColors = ButtonDefaults.textButtonColors(),
+ contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding,
noinline content: @Composable RowScope.() -> Unit
) = Button(
onClick = onClick,
@@ -268,7 +268,7 @@
/**
* Represents the elevation for a button in different states.
*
- * See [ButtonConstants.defaultElevation] for the default elevation used in a [Button].
+ * See [ButtonDefaults.elevation] for the default elevation used in a [Button].
*/
@ExperimentalMaterialApi
@Stable
@@ -285,10 +285,10 @@
/**
* Represents the background and content colors used in a button in different states.
*
- * See [ButtonConstants.defaultButtonColors] for the default colors used in a [Button].
- * See [ButtonConstants.defaultOutlinedButtonColors] for the default colors used in a
+ * See [ButtonDefaults.buttonColors] for the default colors used in a [Button].
+ * See [ButtonDefaults.outlinedButtonColors] for the default colors used in a
* [OutlinedButton].
- * See [ButtonConstants.defaultTextButtonColors] for the default colors used in a [TextButton].
+ * See [ButtonDefaults.textButtonColors] for the default colors used in a [TextButton].
*/
@ExperimentalMaterialApi
@Stable
@@ -311,6 +311,13 @@
/**
* Contains the default values used by [Button]
*/
+@Deprecated(
+ "ButtonConstants has been replaced with ButtonDefaults",
+ ReplaceWith(
+ "ButtonDefaults",
+ "androidx.compose.material.ButtonDefaults"
+ )
+)
object ButtonConstants {
private val ButtonHorizontalPadding = 16.dp
private val ButtonVerticalPadding = 8.dp
@@ -364,6 +371,13 @@
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
+ @Deprecated(
+ "ButtonConstants has been replaced with ButtonDefaults",
+ ReplaceWith(
+ "ButtonDefaults.elevation(elevation, pressedElevation, disabledElevation)",
+ "androidx.compose.material.ButtonDefaults"
+ )
+ )
fun defaultElevation(
defaultElevation: Dp = 2.dp,
pressedElevation: Dp = 8.dp,
@@ -393,6 +407,14 @@
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
+ @Deprecated(
+ "ButtonConstants has been replaced with ButtonDefaults",
+ ReplaceWith(
+ "ButtonDefaults.buttonColors(backgroundColor, disabledBackgroundColor, contentColor, " +
+ "disabledContentColor)",
+ "androidx.compose.material.ButtonDefaults"
+ )
+ )
fun defaultButtonColors(
backgroundColor: Color = MaterialTheme.colors.primary,
disabledBackgroundColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f)
@@ -417,6 +439,14 @@
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
+ @Deprecated(
+ "ButtonConstants has been replaced with ButtonDefaults",
+ ReplaceWith(
+ "ButtonDefaults.outlinedButtonColors(backgroundColor, disabledBackgroundColor, " +
+ "contentColor, disabledContentColor)",
+ "androidx.compose.material.ButtonDefaults"
+ )
+ )
fun defaultOutlinedButtonColors(
backgroundColor: Color = MaterialTheme.colors.surface,
contentColor: Color = MaterialTheme.colors.primary,
@@ -439,6 +469,14 @@
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
+ @Deprecated(
+ "ButtonConstants has been replaced with ButtonDefaults",
+ ReplaceWith(
+ "ButtonDefaults.textButtonColors(backgroundColor, disabledBackgroundColor, " +
+ "contentColor, disabledContentColor)",
+ "androidx.compose.material.ButtonDefaults"
+ )
+ )
fun defaultTextButtonColors(
backgroundColor: Color = Color.Transparent,
contentColor: Color = MaterialTheme.colors.primary,
@@ -464,8 +502,8 @@
/**
* The default disabled content color used by all types of [Button]s
*/
- @Composable
val defaultOutlinedBorder: BorderStroke
+ @Composable
get() = BorderStroke(
OutlinedBorderSize, MaterialTheme.colors.onSurface.copy(alpha = OutlinedBorderOpacity)
)
@@ -482,6 +520,179 @@
}
/**
+ * Contains the default values used by [Button]
+ */
+object ButtonDefaults {
+ private val ButtonHorizontalPadding = 16.dp
+ private val ButtonVerticalPadding = 8.dp
+
+ /**
+ * The default content padding used by [Button]
+ */
+ val ContentPadding = PaddingValues(
+ start = ButtonHorizontalPadding,
+ top = ButtonVerticalPadding,
+ end = ButtonHorizontalPadding,
+ bottom = ButtonVerticalPadding
+ )
+
+ /**
+ * The default min width applied for the [Button].
+ * Note that you can override it by applying Modifier.widthIn directly on [Button].
+ */
+ val MinWidth = 64.dp
+
+ /**
+ * The default min width applied for the [Button].
+ * Note that you can override it by applying Modifier.heightIn directly on [Button].
+ */
+ val MinHeight = 36.dp
+
+ /**
+ * The default size of the icon when used inside a [Button].
+ *
+ * @sample androidx.compose.material.samples.ButtonWithIconSample
+ */
+ val IconSize = 18.dp
+
+ /**
+ * The default size of the spacing between an icon and a text when they used inside a [Button].
+ *
+ * @sample androidx.compose.material.samples.ButtonWithIconSample
+ */
+ val IconSpacing = 8.dp
+
+ // TODO: b/152525426 add support for focused and hovered states
+ /**
+ * Creates a [ButtonElevation] that will animate between the provided values according to the
+ * Material specification for a [Button].
+ *
+ * @param defaultElevation the elevation to use when the [Button] is enabled, and has no
+ * other [Interaction]s.
+ * @param pressedElevation the elevation to use when the [Button] is enabled and
+ * is [Interaction.Pressed].
+ * @param disabledElevation the elevation to use when the [Button] is not enabled.
+ */
+ @OptIn(ExperimentalMaterialApi::class)
+ @Composable
+ fun elevation(
+ defaultElevation: Dp = 2.dp,
+ pressedElevation: Dp = 8.dp,
+ // focused: Dp = 4.dp,
+ // hovered: Dp = 4.dp,
+ disabledElevation: Dp = 0.dp
+ ): ButtonElevation {
+ val clock = AmbientAnimationClock.current.asDisposableClock()
+ return remember(defaultElevation, pressedElevation, disabledElevation, clock) {
+ DefaultButtonElevation(
+ defaultElevation = defaultElevation,
+ pressedElevation = pressedElevation,
+ disabledElevation = disabledElevation,
+ clock = clock
+ )
+ }
+ }
+
+ /**
+ * Creates a [ButtonColors] that represents the default background and content colors used in
+ * a [Button].
+ *
+ * @param backgroundColor the background color of this [Button] when enabled
+ * @param disabledBackgroundColor the background color of this [Button] when not enabled
+ * @param contentColor the content color of this [Button] when enabled
+ * @param disabledContentColor the content color of this [Button] when not enabled
+ */
+ @OptIn(ExperimentalMaterialApi::class)
+ @Composable
+ fun buttonColors(
+ backgroundColor: Color = MaterialTheme.colors.primary,
+ disabledBackgroundColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f)
+ .compositeOver(MaterialTheme.colors.surface),
+ contentColor: Color = contentColorFor(backgroundColor),
+ disabledContentColor: Color = MaterialTheme.colors.onSurface
+ .copy(alpha = ContentAlpha.disabled)
+ ): ButtonColors = DefaultButtonColors(
+ backgroundColor,
+ disabledBackgroundColor,
+ contentColor,
+ disabledContentColor
+ )
+
+ /**
+ * Creates a [ButtonColors] that represents the default background and content colors used in
+ * an [OutlinedButton].
+ *
+ * @param backgroundColor the background color of this [OutlinedButton]
+ * @param contentColor the content color of this [OutlinedButton] when enabled
+ * @param disabledContentColor the content color of this [OutlinedButton] when not enabled
+ */
+ @OptIn(ExperimentalMaterialApi::class)
+ @Composable
+ fun outlinedButtonColors(
+ backgroundColor: Color = MaterialTheme.colors.surface,
+ contentColor: Color = MaterialTheme.colors.primary,
+ disabledContentColor: Color = MaterialTheme.colors.onSurface
+ .copy(alpha = ContentAlpha.disabled)
+ ): ButtonColors = DefaultButtonColors(
+ backgroundColor,
+ backgroundColor,
+ contentColor,
+ disabledContentColor
+ )
+
+ /**
+ * Creates a [ButtonColors] that represents the default background and content colors used in
+ * a [TextButton].
+ *
+ * @param backgroundColor the background color of this [TextButton]
+ * @param contentColor the content color of this [TextButton] when enabled
+ * @param disabledContentColor the content color of this [TextButton] when not enabled
+ */
+ @OptIn(ExperimentalMaterialApi::class)
+ @Composable
+ fun textButtonColors(
+ backgroundColor: Color = Color.Transparent,
+ contentColor: Color = MaterialTheme.colors.primary,
+ disabledContentColor: Color = MaterialTheme.colors.onSurface
+ .copy(alpha = ContentAlpha.disabled)
+ ): ButtonColors = DefaultButtonColors(
+ backgroundColor,
+ backgroundColor,
+ contentColor,
+ disabledContentColor
+ )
+
+ /**
+ * The default color opacity used for an [OutlinedButton]'s border color
+ */
+ const val OutlinedBorderOpacity = 0.12f
+
+ /**
+ * The default [OutlinedButton]'s border size
+ */
+ val OutlinedBorderSize = 1.dp
+
+ /**
+ * The default disabled content color used by all types of [Button]s
+ */
+ val outlinedBorder: BorderStroke
+ @Composable
+ get() = BorderStroke(
+ OutlinedBorderSize, MaterialTheme.colors.onSurface.copy(alpha = OutlinedBorderOpacity)
+ )
+
+ private val TextButtonHorizontalPadding = 8.dp
+
+ /**
+ * The default content padding used by [TextButton]
+ */
+ val TextButtonContentPadding = ContentPadding.copy(
+ start = TextButtonHorizontalPadding,
+ end = TextButtonHorizontalPadding
+ )
+}
+
+/**
* Default [ButtonElevation] implementation.
*/
@OptIn(ExperimentalMaterialApi::class)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Checkbox.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Checkbox.kt
index 6b8cfc3..811d694 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Checkbox.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Checkbox.kt
@@ -76,7 +76,7 @@
* [InteractionState] if you want to read the [InteractionState] and customize the appearance /
* behavior of this Checkbox in different [Interaction]s.
* @param colors [CheckboxColors] that will be used to determine the color of the checkmark / box
- * / border in different states. See [CheckboxConstants.defaultColors].
+ * / border in different states. See [CheckboxDefaults.colors].
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
@@ -86,7 +86,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionState: InteractionState = remember { InteractionState() },
- colors: CheckboxColors = CheckboxConstants.defaultColors()
+ colors: CheckboxColors = CheckboxDefaults.colors()
) {
TriStateCheckbox(
state = ToggleableState(checked),
@@ -120,7 +120,7 @@
* [InteractionState] if you want to read the [InteractionState] and customize the appearance /
* behavior of this TriStateCheckbox in different [Interaction]s.
* @param colors [CheckboxColors] that will be used to determine the color of the checkmark / box
- * / border in different states. See [CheckboxConstants.defaultColors].
+ * / border in different states. See [CheckboxDefaults.colors].
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
@@ -130,7 +130,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionState: InteractionState = remember { InteractionState() },
- colors: CheckboxColors = CheckboxConstants.defaultColors()
+ colors: CheckboxColors = CheckboxDefaults.colors()
) {
CheckboxImpl(
enabled = enabled,
@@ -155,7 +155,7 @@
* Represents the colors used by the three different sections (checkmark, box, and border) of a
* [Checkbox] or [TriStateCheckbox] in different states.
*
- * See [CheckboxConstants.defaultColors] for the default implementation that follows Material
+ * See [CheckboxDefaults.colors] for the default implementation that follows Material
* specifications.
*/
@ExperimentalMaterialApi
@@ -190,6 +190,13 @@
/**
* Constants used in [Checkbox] and [TriStateCheckbox].
*/
+@Deprecated(
+ "CheckboxConstants has been replaced with CheckboxDefaults",
+ ReplaceWith(
+ "CheckboxDefaults",
+ "androidx.compose.material.CheckboxDefaults"
+ )
+)
object CheckboxConstants {
/**
* Creates a [CheckboxColors] that will animate between the provided colors according to the
@@ -204,6 +211,14 @@
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
+ @Deprecated(
+ "CheckboxConstants has been replaced with CheckboxDefaults",
+ ReplaceWith(
+ "CheckboxDefaults.colors(checkedColor, uncheckedColor, checkmarkColor, disabledColor," +
+ " disabledIndeterminateColor)",
+ "androidx.compose.material.CheckboxDefaults"
+ )
+ )
fun defaultColors(
checkedColor: Color = MaterialTheme.colors.secondary,
uncheckedColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
@@ -238,6 +253,57 @@
}
}
+/**
+ * Defaults used in [Checkbox] and [TriStateCheckbox].
+ */
+object CheckboxDefaults {
+ /**
+ * Creates a [CheckboxColors] that will animate between the provided colors according to the
+ * Material specification.
+ *
+ * @param checkedColor the color that will be used for the border and box when checked
+ * @param uncheckedColor color that will be used for the border when unchecked
+ * @param checkmarkColor color that will be used for the checkmark when checked
+ * @param disabledColor color that will be used for the box and border when disabled
+ * @param disabledIndeterminateColor color that will be used for the box and
+ * border in a [TriStateCheckbox] when disabled AND in an [ToggleableState.Indeterminate] state.
+ */
+ @OptIn(ExperimentalMaterialApi::class)
+ @Composable
+ fun colors(
+ checkedColor: Color = MaterialTheme.colors.secondary,
+ uncheckedColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
+ checkmarkColor: Color = MaterialTheme.colors.surface,
+ disabledColor: Color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled),
+ disabledIndeterminateColor: Color = checkedColor.copy(alpha = ContentAlpha.disabled)
+ ): CheckboxColors {
+ val clock = AmbientAnimationClock.current.asDisposableClock()
+ return remember(
+ checkedColor,
+ uncheckedColor,
+ checkmarkColor,
+ disabledColor,
+ disabledIndeterminateColor,
+ clock
+ ) {
+ DefaultCheckboxColors(
+ checkedBorderColor = checkedColor,
+ checkedBoxColor = checkedColor,
+ checkedCheckmarkColor = checkmarkColor,
+ uncheckedCheckmarkColor = checkmarkColor.copy(alpha = 0f),
+ uncheckedBoxColor = checkedColor.copy(alpha = 0f),
+ disabledCheckedBoxColor = disabledColor,
+ disabledUncheckedBoxColor = disabledColor.copy(alpha = 0f),
+ disabledIndeterminateBoxColor = disabledIndeterminateColor,
+ uncheckedBorderColor = uncheckedColor,
+ disabledBorderColor = disabledColor,
+ disabledIndeterminateBorderColor = disabledIndeterminateColor,
+ clock = clock
+ )
+ }
+ }
+}
+
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun CheckboxImpl(
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ContentAlpha.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ContentAlpha.kt
index f4e8a3b..1796da7 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ContentAlpha.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ContentAlpha.kt
@@ -31,8 +31,8 @@
* A high level of content alpha, used to represent high emphasis text such as input text in a
* selected [TextField].
*/
- @Composable
val high: Float
+ @Composable
get() = contentAlpha(
highContrastAlpha = HighContrastContentAlpha.high,
lowContrastAlpha = LowContrastContentAlpha.high
@@ -42,8 +42,8 @@
* A medium level of content alpha, used to represent medium emphasis text such as
* placeholder text in a [TextField].
*/
- @Composable
val medium: Float
+ @Composable
get() = contentAlpha(
highContrastAlpha = HighContrastContentAlpha.medium,
lowContrastAlpha = LowContrastContentAlpha.medium
@@ -53,8 +53,8 @@
* A low level of content alpha used to represent disabled components, such as text in a
* disabled [Button].
*/
- @Composable
val disabled: Float
+ @Composable
get() = contentAlpha(
highContrastAlpha = HighContrastContentAlpha.disabled,
lowContrastAlpha = LowContrastContentAlpha.disabled
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
index 5cb78fb..6c502d2 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
@@ -21,9 +21,9 @@
import androidx.compose.animation.core.AnimationEndReason
import androidx.compose.animation.core.SpringSpec
import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
@@ -34,11 +34,12 @@
import androidx.compose.runtime.savedinstancestate.Saver
import androidx.compose.runtime.savedinstancestate.rememberSavedInstanceState
import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.WithConstraints
+import androidx.compose.ui.gesture.nestedscroll.nestedScroll
import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
import androidx.compose.ui.gesture.tapGestureFilter
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.layout.WithConstraints
import androidx.compose.ui.platform.AmbientAnimationClock
import androidx.compose.ui.platform.AmbientDensity
import androidx.compose.ui.platform.AmbientLayoutDirection
@@ -253,6 +254,8 @@
)
}
+ internal val nestedScrollConnection = this.PreUpPostDownNestedScrollConnection
+
companion object {
/**
* The default [Saver] implementation for [BottomDrawerState].
@@ -342,10 +345,10 @@
drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
gesturesEnabled: Boolean = true,
drawerShape: Shape = MaterialTheme.shapes.large,
- drawerElevation: Dp = DrawerConstants.DefaultElevation,
+ drawerElevation: Dp = DrawerDefaults.Elevation,
drawerBackgroundColor: Color = MaterialTheme.colors.surface,
drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
- scrimColor: Color = DrawerConstants.defaultScrimColor,
+ scrimColor: Color = DrawerDefaults.scrimColor,
bodyContent: @Composable () -> Unit
) {
WithConstraints(modifier.fillMaxSize()) {
@@ -445,10 +448,10 @@
drawerState: BottomDrawerState = rememberBottomDrawerState(BottomDrawerValue.Closed),
gesturesEnabled: Boolean = true,
drawerShape: Shape = MaterialTheme.shapes.large,
- drawerElevation: Dp = DrawerConstants.DefaultElevation,
+ drawerElevation: Dp = DrawerDefaults.Elevation,
drawerBackgroundColor: Color = MaterialTheme.colors.surface,
drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
- scrimColor: Color = DrawerConstants.defaultScrimColor,
+ scrimColor: Color = DrawerDefaults.scrimColor,
bodyContent: @Composable () -> Unit
) {
WithConstraints(modifier.fillMaxSize()) {
@@ -481,13 +484,15 @@
)
}
Box(
- Modifier.swipeable(
- state = drawerState,
- anchors = anchors,
- orientation = Orientation.Vertical,
- enabled = gesturesEnabled,
- resistance = null
- )
+ Modifier
+ .nestedScroll(drawerState.nestedScrollConnection)
+ .swipeable(
+ state = drawerState,
+ anchors = anchors,
+ orientation = Orientation.Vertical,
+ enabled = gesturesEnabled,
+ resistance = null
+ )
) {
Box {
bodyContent()
@@ -530,6 +535,13 @@
/**
* Object to hold default values for [ModalDrawerLayout] and [BottomDrawerLayout]
*/
+@Deprecated(
+ "DrawerConstants has been replaced with DrawerDefaults",
+ ReplaceWith(
+ "DrawerDefaults",
+ "androidx.compose.material.DrawerDefaults"
+ )
+)
object DrawerConstants {
/**
@@ -537,8 +549,8 @@
*/
val DefaultElevation = 16.dp
- @Composable
val defaultScrimColor: Color
+ @Composable
get() = MaterialTheme.colors.onSurface.copy(alpha = ScrimDefaultOpacity)
/**
@@ -547,6 +559,26 @@
const val ScrimDefaultOpacity = 0.32f
}
+/**
+ * Object to hold default values for [ModalDrawerLayout] and [BottomDrawerLayout]
+ */
+object DrawerDefaults {
+
+ /**
+ * Default Elevation for drawer sheet as specified in material specs
+ */
+ val Elevation = 16.dp
+
+ val scrimColor: Color
+ @Composable
+ get() = MaterialTheme.colors.onSurface.copy(alpha = ScrimOpacity)
+
+ /**
+ * Default alpha for scrim color
+ */
+ const val ScrimOpacity = 0.32f
+}
+
private fun calculateFraction(a: Float, b: Float, pos: Float) =
((pos - a) / (b - a)).coerceIn(0f, 1f)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt
index 781185d..daa9ef7 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt
@@ -29,8 +29,8 @@
/**
* Animates the [Dp] value of [this] between [from] and [to] [Interaction]s, to [target]. The
* [AnimationSpec] used depends on the values for [from] and [to], see
- * [ElevationConstants.incomingAnimationSpecForInteraction] and
- * [ElevationConstants.outgoingAnimationSpecForInteraction] for more details.
+ * [ElevationDefaults.incomingAnimationSpecForInteraction] and
+ * [ElevationDefaults.outgoingAnimationSpecForInteraction] for more details.
*
* @param from the previous [Interaction] that was used to calculate elevation. `null` if there
* was no previous [Interaction], such as when the component is in its default state.
@@ -47,9 +47,9 @@
) {
val spec = when {
// Moving to a new state
- to != null -> ElevationConstants.incomingAnimationSpecForInteraction(to)
+ to != null -> ElevationDefaults.incomingAnimationSpecForInteraction(to)
// Moving to default, from a previous state
- from != null -> ElevationConstants.outgoingAnimationSpecForInteraction(from)
+ from != null -> ElevationDefaults.outgoingAnimationSpecForInteraction(from)
// Loading the initial state, or moving back to the baseline state from a disabled /
// unknown state, so just snap to the final value.
else -> null
@@ -62,11 +62,18 @@
*
* Typically you should use [animateElevation] instead, which uses these [AnimationSpec]s
* internally. [animateElevation] in turn is used by the defaults for [Button] and
- * [FloatingActionButton] - inside [ButtonConstants.defaultElevation] and
- * [FloatingActionButtonConstants.defaultElevation] respectively.
+ * [FloatingActionButton] - inside [ButtonDefaults.elevation] and
+ * [FloatingActionButtonDefaults.elevation] respectively.
*
* @see animateElevation
*/
+@Deprecated(
+ "ElevationConstants has been replaced with ElevationDefaults",
+ ReplaceWith(
+ "ElevationDefaults",
+ "androidx.compose.material.ElevationDefaults"
+ )
+)
object ElevationConstants {
/**
* Returns the [AnimationSpec]s used when animating elevation to [interaction], either from a
@@ -99,6 +106,48 @@
}
}
+/**
+ * Contains default [AnimationSpec]s used for animating elevation between different [Interaction]s.
+ *
+ * Typically you should use [animateElevation] instead, which uses these [AnimationSpec]s
+ * internally. [animateElevation] in turn is used by the defaults for [Button] and
+ * [FloatingActionButton] - inside [ButtonDefaults.elevation] and
+ * [FloatingActionButtonDefaults.elevation] respectively.
+ *
+ * @see animateElevation
+ */
+object ElevationDefaults {
+ /**
+ * Returns the [AnimationSpec]s used when animating elevation to [interaction], either from a
+ * previous [Interaction], or from the default state. If [interaction] is unknown, then
+ * returns `null`.
+ *
+ * @param interaction the [Interaction] that is being animated to
+ */
+ fun incomingAnimationSpecForInteraction(interaction: Interaction): AnimationSpec<Dp>? {
+ return when (interaction) {
+ is Interaction.Pressed -> DefaultIncomingSpec
+ is Interaction.Dragged -> DefaultIncomingSpec
+ else -> null
+ }
+ }
+
+ /**
+ * Returns the [AnimationSpec]s used when animating elevation away from [interaction], to the
+ * default state. If [interaction] is unknown, then returns `null`.
+ *
+ * @param interaction the [Interaction] that is being animated away from
+ */
+ fun outgoingAnimationSpecForInteraction(interaction: Interaction): AnimationSpec<Dp>? {
+ return when (interaction) {
+ is Interaction.Pressed -> DefaultOutgoingSpec
+ is Interaction.Dragged -> DefaultOutgoingSpec
+ // TODO: use [HoveredOutgoingSpec] when hovered
+ else -> null
+ }
+ }
+}
+
private val DefaultIncomingSpec = TweenSpec<Dp>(
durationMillis = 120,
easing = FastOutSlowInEasing
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Emphasis.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Emphasis.kt
index 307565a..e92850d 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Emphasis.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Emphasis.kt
@@ -80,18 +80,15 @@
/**
* Emphasis used to express high emphasis, such as for selected text fields.
*/
- @Composable
- val high: Emphasis
+ val high: Emphasis @Composable get
/**
* Emphasis used to express medium emphasis, such as for placeholder text in a text field.
*/
- @Composable
- val medium: Emphasis
+ val medium: Emphasis @Composable get
/**
* Emphasis used to express disabled state, such as for a disabled button.
*/
- @Composable
- val disabled: Emphasis
+ val disabled: Emphasis @Composable get
}
/**
@@ -148,24 +145,24 @@
}
}
- @Composable
override val high: Emphasis
+ @Composable
get() = AlphaEmphasis(
lightTheme = MaterialTheme.colors.isLight,
highContrastAlpha = HighContrastAlphaLevels.high,
reducedContrastAlpha = ReducedContrastAlphaLevels.high
)
- @Composable
override val medium: Emphasis
+ @Composable
get() = AlphaEmphasis(
lightTheme = MaterialTheme.colors.isLight,
highContrastAlpha = HighContrastAlphaLevels.medium,
reducedContrastAlpha = ReducedContrastAlphaLevels.medium
)
- @Composable
override val disabled: Emphasis
+ @Composable
get() = AlphaEmphasis(
lightTheme = MaterialTheme.colors.isLight,
highContrastAlpha = HighContrastAlphaLevels.disabled,
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 ab7e7e6..40fcf2b 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
@@ -17,10 +17,10 @@
package androidx.compose.material
import androidx.compose.animation.AnimatedValueModel
-import androidx.compose.animation.VectorConverter
import androidx.compose.animation.asDisposableClock
import androidx.compose.animation.core.AnimationClockObservable
import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.VectorConverter
import androidx.compose.foundation.AmbientIndication
import androidx.compose.foundation.Interaction
import androidx.compose.foundation.InteractionState
@@ -79,7 +79,7 @@
shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
backgroundColor: Color = MaterialTheme.colors.secondary,
contentColor: Color = contentColorFor(backgroundColor),
- elevation: FloatingActionButtonElevation = FloatingActionButtonConstants.defaultElevation(),
+ elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
content: @Composable () -> Unit
) {
// TODO(aelias): Avoid manually managing the ripple once http://b/157687898
@@ -151,7 +151,7 @@
shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
backgroundColor: Color = MaterialTheme.colors.secondary,
contentColor: Color = contentColorFor(backgroundColor),
- elevation: FloatingActionButtonElevation = FloatingActionButtonConstants.defaultElevation()
+ elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation()
) {
FloatingActionButton(
modifier = modifier.preferredSizeIn(
@@ -188,7 +188,7 @@
/**
* Represents the elevation for a floating action button in different states.
*
- * See [FloatingActionButtonConstants.defaultElevation] for the default elevation used in a
+ * See [FloatingActionButtonDefaults.elevation] for the default elevation used in a
* [FloatingActionButton] and [ExtendedFloatingActionButton].
*/
@ExperimentalMaterialApi
@@ -205,6 +205,13 @@
/**
* Contains the default values used by [FloatingActionButton]
*/
+@Deprecated(
+ "FloatingActionButtonConstants has been replaced with FloatingActionButtonDefaults",
+ ReplaceWith(
+ "FloatingActionButtonDefaults",
+ "androidx.compose.material.FloatingActionButtonDefaults"
+ )
+)
object FloatingActionButtonConstants {
// TODO: b/152525426 add support for focused and hovered states
/**
@@ -218,6 +225,15 @@
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
+ @Deprecated(
+ "FloatingActionButtonConstants has been replaced with " +
+ "FloatingActionButtonDefaults",
+ ReplaceWith(
+ "FloatingActionButtonDefaults.elevation(elevation, pressedElevation, " +
+ "disabledElevation)",
+ "androidx.compose.material.FloatingActionButtonDefaults"
+ )
+ )
fun defaultElevation(
defaultElevation: Dp = 6.dp,
pressedElevation: Dp = 12.dp
@@ -236,6 +252,39 @@
}
/**
+ * Contains the default values used by [FloatingActionButton]
+ */
+object FloatingActionButtonDefaults {
+ // TODO: b/152525426 add support for focused and hovered states
+ /**
+ * Creates a [FloatingActionButtonElevation] that will animate between the provided values
+ * according to the Material specification.
+ *
+ * @param defaultElevation the elevation to use when the [FloatingActionButton] has no
+ * [Interaction]s
+ * @param pressedElevation the elevation to use when the [FloatingActionButton] is
+ * [Interaction.Pressed].
+ */
+ @OptIn(ExperimentalMaterialApi::class)
+ @Composable
+ fun elevation(
+ defaultElevation: Dp = 6.dp,
+ pressedElevation: Dp = 12.dp
+ // focused: Dp = 8.dp,
+ // hovered: Dp = 8.dp,
+ ): FloatingActionButtonElevation {
+ val clock = AmbientAnimationClock.current.asDisposableClock()
+ return remember(defaultElevation, pressedElevation, clock) {
+ DefaultFloatingActionButtonElevation(
+ defaultElevation = defaultElevation,
+ pressedElevation = pressedElevation,
+ clock = clock
+ )
+ }
+ }
+}
+
+/**
* Default [FloatingActionButtonElevation] implementation.
*/
@OptIn(ExperimentalMaterialApi::class)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ListItem.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ListItem.kt
index eaedb99..40b52dc 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ListItem.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ListItem.kt
@@ -111,20 +111,20 @@
private object OneLine {
// TODO(popam): support wide icons
// TODO(popam): convert these to sp
- // List item related constants.
+ // List item related defaults.
private val MinHeight = 48.dp
private val MinHeightWithIcon = 56.dp
- // Icon related constants.
+ // Icon related defaults.
private val IconMinPaddedWidth = 40.dp
private val IconLeftPadding = 16.dp
private val IconVerticalPadding = 8.dp
- // Content related constants.
+ // Content related defaults.
private val ContentLeftPadding = 16.dp
private val ContentRightPadding = 16.dp
- // Trailing related constants.
+ // Trailing related defaults.
private val TrailingRightPadding = 16.dp
@Composable
@@ -166,16 +166,16 @@
}
private object TwoLine {
- // List item related constants.
+ // List item related defaults.
private val MinHeight = 64.dp
private val MinHeightWithIcon = 72.dp
- // Icon related constants.
+ // Icon related defaults.
private val IconMinPaddedWidth = 40.dp
private val IconLeftPadding = 16.dp
private val IconVerticalPadding = 16.dp
- // Content related constants.
+ // Content related defaults.
private val ContentLeftPadding = 16.dp
private val ContentRightPadding = 16.dp
private val OverlineBaselineOffset = 24.dp
@@ -185,7 +185,7 @@
private val PrimaryToSecondaryBaselineOffsetNoIcon = 20.dp
private val PrimaryToSecondaryBaselineOffsetWithIcon = 20.dp
- // Trailing related constants.
+ // Trailing related defaults.
private val TrailingRightPadding = 16.dp
@Composable
@@ -267,15 +267,15 @@
}
private object ThreeLine {
- // List item related constants.
+ // List item related defaults.
private val MinHeight = 88.dp
- // Icon related constants.
+ // Icon related defaults.
private val IconMinPaddedWidth = 40.dp
private val IconLeftPadding = 16.dp
private val IconThreeLineVerticalPadding = 16.dp
- // Content related constants.
+ // Content related defaults.
private val ContentLeftPadding = 16.dp
private val ContentRightPadding = 16.dp
private val ThreeLineBaselineFirstOffset = 28.dp
@@ -283,7 +283,7 @@
private val ThreeLineBaselineThirdOffset = 20.dp
private val ThreeLineTrailingTopPadding = 16.dp
- // Trailing related constants.
+ // Trailing related defaults.
private val TrailingRightPadding = 16.dp
@Composable
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTheme.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTheme.kt
index 0a1c321..8615164 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTheme.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTheme.kt
@@ -94,9 +94,9 @@
*
* @sample androidx.compose.material.samples.ThemeColorSample
*/
- @Composable
- @ComposableContract(readonly = true)
val colors: Colors
+ @Composable
+ @ComposableContract(readonly = true)
get() = AmbientColors.current
/**
@@ -104,17 +104,17 @@
*
* @sample androidx.compose.material.samples.ThemeTextStyleSample
*/
- @Composable
- @ComposableContract(readonly = true)
val typography: Typography
+ @Composable
+ @ComposableContract(readonly = true)
get() = AmbientTypography.current
/**
* Retrieves the current [Shapes] at the call site's position in the hierarchy.
*/
- @Composable
- @ComposableContract(readonly = true)
val shapes: Shapes
+ @Composable
+ @ComposableContract(readonly = true)
get() = AmbientShapes.current
}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
index f075e67..30cbd64 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
@@ -74,7 +74,8 @@
* the [toggle], and then screen end-aligned. Vertically, it will try to expand to the bottom
* of the [toggle], then from the top of the [toggle], and then screen top-aligned. A
* [dropdownOffset] can be provided to adjust the positioning of the menu for cases when the
- * layout bounds of the [toggle] do not coincide with its visual bounds.
+ * layout bounds of the [toggle] do not coincide with its visual bounds. Note the offset will be
+ * applied in the direction in which the menu will decide to expand.
*
* Example usage:
* @sample androidx.compose.material.samples.MenuSample
@@ -199,7 +200,7 @@
}
}
-// Size constants.
+// Size defaults.
private val MenuElevation = 8.dp
private val MenuVerticalMargin = 32.dp
private val DropdownMenuHorizontalPadding = 16.dp
@@ -305,8 +306,8 @@
val contentOffsetY = with(density) { contentOffset.y.toIntPx() }
// Compute horizontal position.
- val toRight = parentGlobalBounds.right + contentOffsetX
- val toLeft = parentGlobalBounds.left - contentOffsetX - popupContentSize.width
+ val toRight = parentGlobalBounds.left + contentOffsetX
+ val toLeft = parentGlobalBounds.right - contentOffsetX - popupContentSize.width
val toDisplayRight = windowGlobalBounds.width - popupContentSize.width
val toDisplayLeft = 0
val x = if (layoutDirection == LayoutDirection.Ltr) {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
index 6e3b519..b8ed48c 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
@@ -23,9 +23,9 @@
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.TweenSpec
import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
@@ -34,6 +34,7 @@
import androidx.compose.runtime.savedinstancestate.Saver
import androidx.compose.runtime.savedinstancestate.rememberSavedInstanceState
import androidx.compose.ui.Modifier
+import androidx.compose.ui.gesture.nestedscroll.nestedScroll
import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
import androidx.compose.ui.gesture.tapGestureFilter
import androidx.compose.ui.graphics.Color
@@ -79,7 +80,7 @@
class ModalBottomSheetState(
initialValue: ModalBottomSheetValue,
clock: AnimationClockObservable,
- animationSpec: AnimationSpec<Float> = SwipeableConstants.DefaultAnimationSpec,
+ animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
) : SwipeableState<ModalBottomSheetValue>(
initialValue = initialValue,
@@ -131,6 +132,8 @@
)
}
+ internal val nestedScrollConnection = this.PreUpPostDownNestedScrollConnection
+
companion object {
/**
* The default [Saver] implementation for [ModalBottomSheetState].
@@ -167,7 +170,7 @@
fun rememberModalBottomSheetState(
initialValue: ModalBottomSheetValue,
clock: AnimationClockObservable = AmbientAnimationClock.current,
- animationSpec: AnimationSpec<Float> = SwipeableConstants.DefaultAnimationSpec,
+ animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
): ModalBottomSheetState {
val disposableClock = clock.asDisposableClock()
@@ -219,10 +222,10 @@
sheetState: ModalBottomSheetState =
rememberModalBottomSheetState(ModalBottomSheetValue.Hidden),
sheetShape: Shape = MaterialTheme.shapes.large,
- sheetElevation: Dp = ModalBottomSheetConstants.DefaultElevation,
+ sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,
sheetBackgroundColor: Color = MaterialTheme.colors.surface,
sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
- scrimColor: Color = ModalBottomSheetConstants.DefaultScrimColor,
+ scrimColor: Color = ModalBottomSheetDefaults.scrimColor,
content: @Composable () -> Unit
) = BottomSheetStack(
modifier = modifier,
@@ -230,6 +233,7 @@
Surface(
Modifier
.fillMaxWidth()
+ .nestedScroll(sheetState.nestedScrollConnection)
.offset(y = { sheetState.offset.value }),
shape = sheetShape,
elevation = sheetElevation,
@@ -322,6 +326,13 @@
/**
* Contains useful constants for [ModalBottomSheetLayout].
*/
+@Deprecated(
+ "ModalBottomSheetConstants has been replaced with ModalBottomSheetDefaults",
+ ReplaceWith(
+ "ModalBottomSheetDefaults",
+ "androidx.compose.material.ModalBottomSheetDefaults"
+ )
+)
object ModalBottomSheetConstants {
/**
@@ -332,7 +343,25 @@
/**
* The default scrim color used by [ModalBottomSheetLayout].
*/
- @Composable
val DefaultScrimColor: Color
+ @Composable
+ get() = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
+}
+
+/**
+ * Contains useful Defaults for [ModalBottomSheetLayout].
+ */
+object ModalBottomSheetDefaults {
+
+ /**
+ * The default elevation used by [ModalBottomSheetLayout].
+ */
+ val Elevation = 16.dp
+
+ /**
+ * The default scrim color used by [ModalBottomSheetLayout].
+ */
+ val scrimColor: Color
+ @Composable
get() = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
index fa19662..653593a 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
@@ -30,7 +30,6 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.RectangleShape
@@ -274,7 +273,6 @@
)
}
-@OptIn(ExperimentalFocus::class)
@Composable
internal fun OutlinedTextFieldLayout(
modifier: Modifier = Modifier,
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ProgressIndicator.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ProgressIndicator.kt
index 09a97e0..096155b 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ProgressIndicator.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ProgressIndicator.kt
@@ -16,22 +16,21 @@
package androidx.compose.material
-import androidx.compose.animation.core.AnimationConstants.Infinite
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.FloatPropKey
import androidx.compose.animation.core.IntPropKey
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.SpringSpec
+import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes
-import androidx.compose.animation.core.repeatable
import androidx.compose.animation.core.transitionDefinition
import androidx.compose.animation.core.tween
import androidx.compose.animation.transition
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.preferredSize
import androidx.compose.foundation.progressSemantics
-import androidx.compose.material.ProgressIndicatorConstants.DefaultIndicatorBackgroundOpacity
+import androidx.compose.material.ProgressIndicatorDefaults.IndicatorBackgroundOpacity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@@ -53,7 +52,7 @@
* A determinate linear progress indicator that represents progress by drawing a horizontal line.
*
* By default there is no animation between [progress] values. You can use
- * [ProgressIndicatorConstants.DefaultProgressAnimationSpec] as the default recommended
+ * [ProgressIndicatorDefaults.ProgressAnimationSpec] as the default recommended
* [AnimationSpec] when animating progress, such as in the following example:
*
* @sample androidx.compose.material.samples.LinearProgressIndicatorSample
@@ -69,14 +68,14 @@
@FloatRange(from = 0.0, to = 1.0) progress: Float,
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colors.primary,
- backgroundColor: Color = color.copy(alpha = DefaultIndicatorBackgroundOpacity)
+ backgroundColor: Color = color.copy(alpha = IndicatorBackgroundOpacity)
) {
Canvas(
modifier
.progressSemantics(progress)
.preferredSize(LinearIndicatorWidth, LinearIndicatorHeight)
) {
- val strokeWidth = ProgressIndicatorConstants.DefaultStrokeWidth.toPx()
+ val strokeWidth = ProgressIndicatorDefaults.StrokeWidth.toPx()
drawLinearIndicatorBackground(backgroundColor, strokeWidth)
drawLinearIndicator(0f, progress, color, strokeWidth)
}
@@ -94,7 +93,7 @@
fun LinearProgressIndicator(
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colors.primary,
- backgroundColor: Color = color.copy(alpha = DefaultIndicatorBackgroundOpacity)
+ backgroundColor: Color = color.copy(alpha = IndicatorBackgroundOpacity)
) {
val state = transition(
definition = LinearIndeterminateTransition,
@@ -110,7 +109,7 @@
val firstLineTail = state[FirstLineTailProp]
val secondLineHead = state[SecondLineHeadProp]
val secondLineTail = state[SecondLineTailProp]
- val strokeWidth = ProgressIndicatorConstants.DefaultStrokeWidth.toPx()
+ val strokeWidth = ProgressIndicatorDefaults.StrokeWidth.toPx()
drawLinearIndicatorBackground(backgroundColor, strokeWidth)
if (firstLineHead - firstLineTail > 0) {
drawLinearIndicator(
@@ -160,7 +159,7 @@
* 0 to 360 degrees.
*
* By default there is no animation between [progress] values. You can use
- * [ProgressIndicatorConstants.DefaultProgressAnimationSpec] as the default recommended
+ * [ProgressIndicatorDefaults.ProgressAnimationSpec] as the default recommended
* [AnimationSpec] when animating progress, such as in the following example:
*
* @sample androidx.compose.material.samples.CircularProgressIndicatorSample
@@ -175,7 +174,7 @@
@FloatRange(from = 0.0, to = 1.0) progress: Float,
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colors.primary,
- strokeWidth: Dp = ProgressIndicatorConstants.DefaultStrokeWidth
+ strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth
) {
val stroke = with(AmbientDensity.current) {
Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Butt)
@@ -203,7 +202,7 @@
fun CircularProgressIndicator(
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colors.primary,
- strokeWidth: Dp = ProgressIndicatorConstants.DefaultStrokeWidth
+ strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth
) {
val stroke = with(AmbientDensity.current) {
Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Square)
@@ -259,6 +258,13 @@
/**
* Contains the default values used for [LinearProgressIndicator] and [CircularProgressIndicator].
*/
+@Deprecated(
+ "ProgressIndicatorConstants has been replaced with ProgressIndicatorDefaults",
+ ReplaceWith(
+ "ProgressIndicatorDefaults",
+ "androidx.compose.material.ProgressIndicatorDefaults"
+ )
+)
object ProgressIndicatorConstants {
/**
* Default stroke width for [CircularProgressIndicator], and default height for
@@ -288,6 +294,38 @@
)
}
+/**
+ * Contains the default values used for [LinearProgressIndicator] and [CircularProgressIndicator].
+ */
+object ProgressIndicatorDefaults {
+ /**
+ * Default stroke width for [CircularProgressIndicator], and default height for
+ * [LinearProgressIndicator].
+ *
+ * This can be customized with the `strokeWidth` parameter on [CircularProgressIndicator],
+ * and by passing a layout modifier setting the height for [LinearProgressIndicator].
+ */
+ val StrokeWidth = 4.dp
+
+ /**
+ * The default opacity applied to the indicator color to create the background color in a
+ * [LinearProgressIndicator].
+ */
+ const val IndicatorBackgroundOpacity = 0.24f
+
+ /**
+ * The default [AnimationSpec] that should be used when animating between progress in a
+ * determinate progress indicator.
+ */
+ val ProgressAnimationSpec = SpringSpec(
+ dampingRatio = Spring.DampingRatioNoBouncy,
+ stiffness = Spring.StiffnessVeryLow,
+ // The default threshold is 0.01, or 1% of the overall progress range, which is quite
+ // large and noticeable.
+ visibilityThreshold = 1 / 1000f
+ )
+}
+
private fun DrawScope.drawDeterminateCircularIndicator(
startAngle: Float,
sweep: Float,
@@ -322,7 +360,7 @@
// LinearProgressIndicator Material specs
// TODO: there are currently 3 fixed widths in Android, should this be flexible? Material says
// the width should be 240dp here.
-private val LinearIndicatorHeight = ProgressIndicatorConstants.DefaultStrokeWidth
+private val LinearIndicatorHeight = ProgressIndicatorDefaults.StrokeWidth
private val LinearIndicatorWidth = 240.dp
// CircularProgressIndicator Material specs
@@ -373,32 +411,28 @@
}
transition(fromState = 0, toState = 1) {
- FirstLineHeadProp using repeatable(
- iterations = Infinite,
+ FirstLineHeadProp using infiniteRepeatable(
animation = keyframes {
durationMillis = LinearAnimationDuration
0f at FirstLineHeadDelay with FirstLineHeadEasing
1f at FirstLineHeadDuration + FirstLineHeadDelay
}
)
- FirstLineTailProp using repeatable(
- iterations = Infinite,
+ FirstLineTailProp using infiniteRepeatable(
animation = keyframes {
durationMillis = LinearAnimationDuration
0f at FirstLineTailDelay with FirstLineTailEasing
1f at FirstLineTailDuration + FirstLineTailDelay
}
)
- SecondLineHeadProp using repeatable(
- iterations = Infinite,
+ SecondLineHeadProp using infiniteRepeatable(
animation = keyframes {
durationMillis = LinearAnimationDuration
0f at SecondLineHeadDelay with SecondLineHeadEasing
1f at SecondLineHeadDuration + SecondLineHeadDelay
}
)
- SecondLineTailProp using repeatable(
- iterations = Infinite,
+ SecondLineTailProp using infiniteRepeatable(
animation = keyframes {
durationMillis = LinearAnimationDuration
0f at SecondLineTailDelay with SecondLineTailEasing
@@ -462,30 +496,26 @@
}
transition(fromState = 0, toState = 1) {
- IterationProp using repeatable(
- iterations = Infinite,
+ IterationProp using infiniteRepeatable(
animation = tween(
durationMillis = RotationDuration * RotationsPerCycle,
easing = LinearEasing
)
)
- BaseRotationProp using repeatable(
- iterations = Infinite,
+ BaseRotationProp using infiniteRepeatable(
animation = tween(
durationMillis = RotationDuration,
easing = LinearEasing
)
)
- HeadRotationProp using repeatable(
- iterations = Infinite,
+ HeadRotationProp using infiniteRepeatable(
animation = keyframes {
durationMillis = HeadAndTailAnimationDuration + HeadAndTailDelayDuration
0f at 0 with CircularEasing
JumpRotationAngle at HeadAndTailAnimationDuration
}
)
- TailRotationProp using repeatable(
- iterations = Infinite,
+ TailRotationProp using infiniteRepeatable(
animation = keyframes {
durationMillis = HeadAndTailAnimationDuration + HeadAndTailDelayDuration
0f at HeadAndTailDelayDuration with CircularEasing
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/RadioButton.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/RadioButton.kt
index 6e9d2ed..61ba138 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/RadioButton.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/RadioButton.kt
@@ -66,7 +66,7 @@
* [InteractionState] if you want to read the [InteractionState] and customize the appearance /
* behavior of this RadioButton in different [Interaction]s.
* @param colors [RadioButtonColors] that will be used to resolve the color used for this
- * RadioButton in different states. See [RadioButtonConstants.defaultColors].
+ * RadioButton in different states. See [RadioButtonDefaults.colors].
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
@@ -76,7 +76,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionState: InteractionState = remember { InteractionState() },
- colors: RadioButtonColors = RadioButtonConstants.defaultColors()
+ colors: RadioButtonColors = RadioButtonDefaults.colors()
) {
val dotRadius = animate(
target = if (selected) RadioButtonDotSize / 2 else 0.dp,
@@ -106,7 +106,7 @@
/**
* Represents the color used by a [RadioButton] in different states.
*
- * See [RadioButtonConstants.defaultColors] for the default implementation that follows Material
+ * See [RadioButtonDefaults.colors] for the default implementation that follows Material
* specifications.
*/
@ExperimentalMaterialApi
@@ -125,6 +125,13 @@
/**
* Constants used in [RadioButton].
*/
+@Deprecated(
+ "RadioButtonConstants has been replaced with RadioButtonDefaults",
+ ReplaceWith(
+ "RadioButtonDefaults",
+ "androidx.compose.material.RadioButtonDefaults"
+ )
+)
object RadioButtonConstants {
/**
* Creates a [RadioButtonColors] that will animate between the provided colors according to
@@ -137,6 +144,13 @@
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
+ @Deprecated(
+ "RadioButtonConstants has been replaced with RadioButtonDefaults",
+ ReplaceWith(
+ "RadioButtonDefaults.colors(selectedColor, unselectedColor, disabledColor)",
+ "androidx.compose.material.RadioButtonDefaults"
+ )
+ )
fun defaultColors(
selectedColor: Color = MaterialTheme.colors.secondary,
unselectedColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
@@ -154,6 +168,38 @@
}
}
+/**
+ * Defaults used in [RadioButton].
+ */
+object RadioButtonDefaults {
+ /**
+ * Creates a [RadioButtonColors] that will animate between the provided colors according to
+ * the Material specification.
+ *
+ * @param selectedColor the color to use for the RadioButton when selected and enabled.
+ * @param unselectedColor the color to use for the RadioButton when unselected and enabled.
+ * @param disabledColor the color to use for the RadioButton when disabled.
+ * @return the resulting [Color] used for the RadioButton
+ */
+ @OptIn(ExperimentalMaterialApi::class)
+ @Composable
+ fun colors(
+ selectedColor: Color = MaterialTheme.colors.secondary,
+ unselectedColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
+ disabledColor: Color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
+ ): RadioButtonColors {
+ val clock = AmbientAnimationClock.current.asDisposableClock()
+ return remember(
+ selectedColor,
+ unselectedColor,
+ disabledColor,
+ clock
+ ) {
+ DefaultRadioButtonColors(selectedColor, unselectedColor, disabledColor, clock)
+ }
+ }
+}
+
private fun DrawScope.drawRadio(color: Color, dotRadius: Dp) {
val strokeWidth = RadioStrokeWidth.toPx()
drawCircle(color, RadioRadius.toPx() - strokeWidth / 2, style = Stroke(strokeWidth))
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
index 33f7dd15..00f6b24 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
@@ -160,10 +160,10 @@
drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
drawerGesturesEnabled: Boolean = true,
drawerShape: Shape = MaterialTheme.shapes.large,
- drawerElevation: Dp = DrawerConstants.DefaultElevation,
+ drawerElevation: Dp = DrawerDefaults.Elevation,
drawerBackgroundColor: Color = MaterialTheme.colors.surface,
drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
- drawerScrimColor: Color = DrawerConstants.defaultScrimColor,
+ drawerScrimColor: Color = DrawerDefaults.scrimColor,
backgroundColor: Color = MaterialTheme.colors.background,
contentColor: Color = contentColorFor(backgroundColor),
bodyContent: @Composable (PaddingValues) -> Unit
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 31d5c18..c6b155b 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
@@ -40,8 +40,8 @@
import androidx.compose.foundation.layout.preferredSize
import androidx.compose.foundation.layout.preferredWidthIn
import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.SliderConstants.InactiveTrackColorAlpha
-import androidx.compose.material.SliderConstants.TickColorAlpha
+import androidx.compose.material.SliderDefaults.InactiveTrackColorAlpha
+import androidx.compose.material.SliderDefaults.TickColorAlpha
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@@ -59,8 +59,8 @@
import androidx.compose.ui.platform.AmbientDensity
import androidx.compose.ui.platform.AmbientLayoutDirection
import androidx.compose.ui.semantics.AccessibilityRangeInfo
-import androidx.compose.ui.semantics.accessibilityValue
-import androidx.compose.ui.semantics.accessibilityValueRange
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.semantics.stateDescriptionRange
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.setProgress
import androidx.compose.ui.unit.LayoutDirection
@@ -196,6 +196,13 @@
/**
* Object to hold constants used by the [Slider]
*/
+@Deprecated(
+ "SliderConstants has been replaced with SliderDefaults",
+ ReplaceWith(
+ "SliderDefaults",
+ "androidx.compose.material.SliderDefaults"
+ )
+)
object SliderConstants {
/**
* Default alpha of the inactive part of the track
@@ -208,6 +215,21 @@
const val TickColorAlpha = 0.54f
}
+/**
+ * Object to hold defaults used by [Slider]
+ */
+object SliderDefaults {
+ /**
+ * Default alpha of the inactive part of the track
+ */
+ const val InactiveTrackColorAlpha = 0.24f
+
+ /**
+ * Default alpha of the ticks that are drawn on top of the track
+ */
+ const val TickColorAlpha = 0.54f
+}
+
@Composable
private fun SliderImpl(
positionFraction: Float,
@@ -361,8 +383,8 @@
else -> (fraction * 100).roundToInt().coerceIn(1, 99)
}
return semantics(mergeDescendants = true) {
- accessibilityValue = Strings.TemplatePercent.format(percent)
- accessibilityValueRange = AccessibilityRangeInfo(coerced, valueRange, steps)
+ stateDescription = Strings.TemplatePercent.format(percent)
+ stateDescriptionRange = AccessibilityRangeInfo(coerced, valueRange, steps)
setProgress(
action = { targetValue ->
val newValue = targetValue.coerceIn(position.startValue, position.endValue)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
index 41a9958..dcdafed 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
@@ -59,7 +59,7 @@
*
* @param modifier modifiers for the the Snackbar layout
* @param action action / button component to add as an action to the snackbar. Consider using
- * [SnackbarConstants.defaultActionPrimaryColor] as the color for the action, if you do not
+ * [SnackbarDefaults.primaryActionColor] as the color for the action, if you do not
* have a predefined color you wish to use instead.
* @param actionOnNewLine whether or not action should be put on the separate line. Recommended
* for action with long action text
@@ -79,7 +79,7 @@
action: @Composable (() -> Unit)? = null,
actionOnNewLine: Boolean = false,
shape: Shape = MaterialTheme.shapes.small,
- backgroundColor: Color = SnackbarConstants.defaultBackgroundColor,
+ backgroundColor: Color = SnackbarDefaults.backgroundColor,
contentColor: Color = MaterialTheme.colors.surface,
elevation: Dp = 6.dp,
text: @Composable () -> Unit
@@ -147,16 +147,16 @@
modifier: Modifier = Modifier,
actionOnNewLine: Boolean = false,
shape: Shape = MaterialTheme.shapes.small,
- backgroundColor: Color = SnackbarConstants.defaultBackgroundColor,
+ backgroundColor: Color = SnackbarDefaults.backgroundColor,
contentColor: Color = MaterialTheme.colors.surface,
- actionColor: Color = SnackbarConstants.defaultActionPrimaryColor,
+ actionColor: Color = SnackbarDefaults.primaryActionColor,
elevation: Dp = 6.dp
) {
val actionLabel = snackbarData.actionLabel
val actionComposable: (@Composable () -> Unit)? = if (actionLabel != null) {
{
TextButton(
- colors = ButtonConstants.defaultTextButtonColors(contentColor = actionColor),
+ colors = ButtonDefaults.textButtonColors(contentColor = actionColor),
onClick = { snackbarData.performAction() },
content = { Text(actionLabel) }
)
@@ -179,6 +179,13 @@
/**
* Object to hold constants used by the [Snackbar]
*/
+@Deprecated(
+ "SnackbarConstants has been replaced with SnackbarDefaults",
+ ReplaceWith(
+ "SnackbarDefaults",
+ "androidx.compose.material.SnackbarDefaults"
+ )
+)
object SnackbarConstants {
/**
@@ -189,8 +196,8 @@
/**
* Default background color of the [Snackbar]
*/
- @Composable
val defaultBackgroundColor: Color
+ @Composable
get() =
MaterialTheme.colors.onSurface
.copy(alpha = SnackbarOverlayAlpha)
@@ -210,8 +217,57 @@
* [MaterialTheme.colors] to attempt to reduce the contrast, and when in a dark theme this
* function uses [Colors.primaryVariant].
*/
- @Composable
val defaultActionPrimaryColor: Color
+ @Composable
+ get() {
+ val colors = MaterialTheme.colors
+ return if (colors.isLight) {
+ val primary = colors.primary
+ val overlayColor = colors.surface.copy(alpha = 0.6f)
+
+ overlayColor.compositeOver(primary)
+ } else {
+ colors.primaryVariant
+ }
+ }
+}
+
+/**
+ * Object to hold defaults used by [Snackbar]
+ */
+object SnackbarDefaults {
+
+ /**
+ * Default alpha of the overlay applied to the [backgroundColor]
+ */
+ private const val SnackbarOverlayAlpha = 0.8f
+
+ /**
+ * Default background color of the [Snackbar]
+ */
+ val backgroundColor: Color
+ @Composable
+ get() =
+ MaterialTheme.colors.onSurface
+ .copy(alpha = SnackbarOverlayAlpha)
+ .compositeOver(MaterialTheme.colors.surface)
+
+ /**
+ * Provides a best-effort 'primary' color to be used as the primary color inside a [Snackbar].
+ * Given that [Snackbar]s have an 'inverted' theme, i.e in a light theme they appear dark, and
+ * in a dark theme they appear light, just using [Colors.primary] will not work, and has
+ * incorrect contrast.
+ *
+ * If your light theme has a corresponding dark theme, you should instead directly use
+ * [Colors.primary] from the dark theme when in a light theme, and use
+ * [Colors.primaryVariant] from the dark theme when in a dark theme.
+ *
+ * When in a light theme, this function applies a color overlay to [Colors.primary] from
+ * [MaterialTheme.colors] to attempt to reduce the contrast, and when in a dark theme this
+ * function uses [Colors.primaryVariant].
+ */
+ val primaryActionColor: Color
+ @Composable
get() {
val colors = MaterialTheme.colors
return if (colors.isLight) {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeToDismiss.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeToDismiss.kt
index b0b7b05..200bd21c2 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeToDismiss.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeToDismiss.kt
@@ -28,8 +28,8 @@
import androidx.compose.material.DismissValue.Default
import androidx.compose.material.DismissValue.DismissedToEnd
import androidx.compose.material.DismissValue.DismissedToStart
-import androidx.compose.material.SwipeableConstants.StandardResistanceFactor
-import androidx.compose.material.SwipeableConstants.StiffResistanceFactor
+import androidx.compose.material.SwipeableDefaults.StandardResistanceFactor
+import androidx.compose.material.SwipeableDefaults.StiffResistanceFactor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.savedinstancestate.Saver
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
index 858066e..2bcbfb2 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
@@ -25,10 +25,10 @@
import androidx.compose.animation.core.SpringSpec
import androidx.compose.foundation.InteractionState
import androidx.compose.foundation.gestures.draggable
-import androidx.compose.material.SwipeableConstants.DefaultAnimationSpec
-import androidx.compose.material.SwipeableConstants.DefaultVelocityThreshold
-import androidx.compose.material.SwipeableConstants.StandardResistanceFactor
-import androidx.compose.material.SwipeableConstants.defaultResistanceConfig
+import androidx.compose.material.SwipeableDefaults.AnimationSpec
+import androidx.compose.material.SwipeableDefaults.VelocityThreshold
+import androidx.compose.material.SwipeableDefaults.StandardResistanceFactor
+import androidx.compose.material.SwipeableDefaults.resistanceConfig
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
@@ -42,12 +42,16 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollSource
import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
import androidx.compose.ui.platform.AmbientAnimationClock
import androidx.compose.ui.platform.AmbientDensity
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.annotation.FloatRange
import androidx.compose.ui.util.lerp
@@ -73,7 +77,7 @@
open class SwipeableState<T>(
initialValue: T,
clock: AnimationClockObservable,
- internal val animationSpec: AnimationSpec<Float> = DefaultAnimationSpec,
+ internal val animationSpec: AnimationSpec<Float> = AnimationSpec,
internal val confirmStateChange: (newValue: T) -> Boolean = { true }
) {
/**
@@ -109,8 +113,8 @@
*/
val overflow: State<Float> get() = overflowState
- private var minBound = Float.NEGATIVE_INFINITY
- private var maxBound = Float.POSITIVE_INFINITY
+ internal var minBound = Float.NEGATIVE_INFINITY
+ internal var maxBound = Float.POSITIVE_INFINITY
private val anchorsState = mutableStateOf(emptyMap<Float, T>())
@@ -167,6 +171,8 @@
internal var thresholds: (Float, Float) -> Float by mutableStateOf({ _, _ -> 0f })
+ internal var velocityThreshold by mutableStateOf(0f)
+
internal var resistance: ResistanceConfig? by mutableStateOf(null)
internal val holder: AnimatedFloat = NotificationBasedAnimatedFloat(0f, animationClockProxy) {
@@ -290,6 +296,63 @@
}
}
+ /**
+ * Perform fling with settling to one of the anchors which is determined by the given
+ * [velocity]. Fling with settling [swipeable] will always consume all the velocity provided
+ * since it will settle at the anchor.
+ *
+ * In general cases, [swipeable] flings by itself when being swiped. This method is to be
+ * used for nested scroll logic that wraps the [swipeable]. In nested scroll developer may
+ * want to trigger settling fling when the child scroll container reaches the bound.
+ *
+ * @param velocity velocity to fling and settle with
+ * @param onEnd callback to be invoked when fling is completed
+ */
+ fun performFling(velocity: Float, onEnd: (() -> Unit)) {
+ val lastAnchor = anchors.getOffset(value)!!
+ val targetValue = computeTarget(
+ offset = offset.value,
+ lastValue = lastAnchor,
+ anchors = anchors.keys,
+ thresholds = thresholds,
+ velocity = velocity,
+ velocityThreshold = velocityThreshold
+ )
+ val targetState = anchors[targetValue]
+ if (targetState != null && confirmStateChange(targetState)) {
+ animateTo(targetState, onEnd = { _, _ -> onEnd() })
+ } else {
+ // If the user vetoed the state change, rollback to the previous state.
+ holder.animateTo(lastAnchor, animationSpec, onEnd = { _, _ -> onEnd() })
+ }
+ }
+
+ /**
+ * Force [swipeable] to consume drag delta provided from outside of the regular [swipeable]
+ * gesture flow.
+ *
+ * Note: This method performs generic drag and it won't settle to any particular anchor, *
+ * leaving swipeable in between anchors. When done dragging, [performFling] must be
+ * called as well to ensure swipeable will settle at the anchor.
+ *
+ * In general cases, [swipeable] drags by itself when being swiped. This method is to be
+ * used for nested scroll logic that wraps the [swipeable]. In nested scroll developer may
+ * want to force drag when the child scroll container reaches the bound.
+ *
+ * @param delta delta in pixels to drag by
+ *
+ * @return the amount of [delta] consumed
+ */
+ fun performDrag(delta: Float): Float {
+ val potentiallyConsumed = holder.value + delta
+ val clamped = potentiallyConsumed.coerceIn(minBound, maxBound)
+ val deltaToConsume = clamped - holder.value
+ if (abs(deltaToConsume) > 0) {
+ holder.snapTo(holder.value + deltaToConsume)
+ }
+ return deltaToConsume
+ }
+
companion object {
/**
* The default [Saver] implementation for [SwipeableState].
@@ -333,7 +396,7 @@
@ExperimentalMaterialApi
fun <T : Any> rememberSwipeableState(
initialValue: T,
- animationSpec: AnimationSpec<Float> = DefaultAnimationSpec,
+ animationSpec: AnimationSpec<Float> = AnimationSpec,
confirmStateChange: (newValue: T) -> Boolean = { true }
): SwipeableState<T> {
val clock = AmbientAnimationClock.current.asDisposableClock()
@@ -367,7 +430,7 @@
internal fun <T : Any> rememberSwipeableStateFor(
value: T,
onValueChange: (T) -> Unit,
- animationSpec: AnimationSpec<Float> = DefaultAnimationSpec
+ animationSpec: AnimationSpec<Float> = AnimationSpec
): SwipeableState<T> {
val swipeableState = rememberSwipeableState(
initialValue = value,
@@ -433,8 +496,8 @@
reverseDirection: Boolean = false,
interactionState: InteractionState? = null,
thresholds: (from: T, to: T) -> ThresholdConfig = { _, _ -> FixedThreshold(56.dp) },
- resistance: ResistanceConfig? = defaultResistanceConfig(anchors.keys),
- velocityThreshold: Dp = DefaultVelocityThreshold
+ resistance: ResistanceConfig? = resistanceConfig(anchors.keys),
+ velocityThreshold: Dp = VelocityThreshold
) = composed(
inspectorInfo = debugInspectorInfo {
name = "swipeable"
@@ -463,6 +526,9 @@
val to = anchors.getValue(b)
with(thresholds(from, to)) { density.computeThreshold(a, b) }
}
+ with(density) {
+ state.velocityThreshold = velocityThreshold.toPx()
+ }
}
onCommit {
state.resistance = resistance
@@ -475,22 +541,7 @@
interactionState = interactionState,
startDragImmediately = state.isAnimationRunning,
onDragStopped = { velocity ->
- val lastAnchor = anchors.getOffset(state.value)!!
- val targetValue = computeTarget(
- offset = state.offset.value,
- lastValue = lastAnchor,
- anchors = anchors.keys,
- thresholds = state.thresholds,
- velocity = velocity,
- velocityThreshold = with(density) { velocityThreshold.toPx() }
- )
- val targetState = anchors[targetValue]
- if (targetState != null && state.confirmStateChange(targetState)) {
- state.animateTo(targetState)
- } else {
- // If the user vetoed the state change, rollback to the previous state.
- state.holder.animateTo(lastAnchor, state.animationSpec)
- }
+ state.performFling(velocity) {}
}
) { delta ->
state.holder.snapTo(state.holder.value + delta)
@@ -548,10 +599,10 @@
*
* The resistance basis is usually either the size of the component which [swipeable] is applied
* to, or the distance between the minimum and maximum anchors. For a constructor in which the
- * resistance basis defaults to the latter, consider using [defaultResistanceConfig].
+ * resistance basis defaults to the latter, consider using [resistanceConfig].
*
* You may specify different resistance factors for each bound. Consider using one of the default
- * resistance factors in [SwipeableConstants]: `StandardResistanceFactor` to convey that the user
+ * resistance factors in [SwipeableDefaults]: `StandardResistanceFactor` to convey that the user
* has run out of things to see, and `StiffResistanceFactor` to convey that the user cannot swipe
* this right now. Also, you can set either factor to 0 to disable resistance at that bound.
*
@@ -610,9 +661,9 @@
offset: Float,
anchors: Set<Float>
): List<Float> {
- // Find the anchors the target lies between.
- val a = anchors.filter { it <= offset }.maxOrNull()
- val b = anchors.filter { it >= offset }.minOrNull()
+ // Find the anchors the target lies between with a little bit of rounding error.
+ val a = anchors.filter { it <= offset + 0.001 }.maxOrNull()
+ val b = anchors.filter { it >= offset - 0.001 }.minOrNull()
return when {
a == null ->
@@ -673,6 +724,13 @@
/**
* Contains useful constants for [swipeable] and [SwipeableState].
*/
+@Deprecated(
+ "SwipeableConstants has been replaced with SwipeableDefaults",
+ ReplaceWith(
+ "SwipeableDefaults",
+ "androidx.compose.material.SwipeableDefaults"
+ )
+)
object SwipeableConstants {
/**
* The default animation used by [SwipeableState].
@@ -712,4 +770,101 @@
ResistanceConfig(basis, factorAtMin, factorAtMax)
}
}
-}
\ No newline at end of file
+}
+
+/**
+ * Contains useful defaults for [swipeable] and [SwipeableState].
+ */
+object SwipeableDefaults {
+ /**
+ * The default animation used by [SwipeableState].
+ */
+ val AnimationSpec = SpringSpec<Float>()
+
+ /**
+ * The default velocity threshold (1.8 dp per millisecond) used by [swipeable].
+ */
+ val VelocityThreshold = 125.dp
+
+ /**
+ * A stiff resistance factor which indicates that swiping isn't available right now.
+ */
+ const val StiffResistanceFactor = 20f
+
+ /**
+ * A standard resistance factor which indicates that the user has run out of things to see.
+ */
+ const val StandardResistanceFactor = 10f
+
+ /**
+ * The default resistance config used by [swipeable].
+ *
+ * This returns `null` if there is one anchor. If there are at least two anchors, it returns
+ * a [ResistanceConfig] with the resistance basis equal to the distance between the two bounds.
+ */
+ fun resistanceConfig(
+ anchors: Set<Float>,
+ factorAtMin: Float = StandardResistanceFactor,
+ factorAtMax: Float = StandardResistanceFactor
+ ): ResistanceConfig? {
+ return if (anchors.size <= 1) {
+ null
+ } else {
+ val basis = anchors.maxOrNull()!! - anchors.minOrNull()!!
+ ResistanceConfig(basis, factorAtMin, factorAtMax)
+ }
+ }
+}
+
+// temp default nested scroll connection for swipeables which desire as an opt in
+// revisit in b/174756744 as all types will have their own specific connection probably
+@ExperimentalMaterialApi
+internal val <T> SwipeableState<T>.PreUpPostDownNestedScrollConnection: NestedScrollConnection
+ get() = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ val delta = available.toFloat()
+ return if (delta < 0 && source == NestedScrollSource.Drag) {
+ performDrag(delta).toOffset()
+ } else {
+ Offset.Zero
+ }
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ return if (source == NestedScrollSource.Drag) {
+ performDrag(available.toFloat()).toOffset()
+ } else {
+ Offset.Zero
+ }
+ }
+
+ override fun onPreFling(available: Velocity): Velocity {
+ val toFling = available.pixelsPerSecond.toFloat()
+ return if (toFling < 0 && offset.value > minBound) {
+ performFling(velocity = toFling) {}
+ // since we go to the anchor with tween settling, consume all for the best UX
+ available
+ } else {
+ Velocity.Zero
+ }
+ }
+
+ override fun onPostFling(
+ consumed: Velocity,
+ available: Velocity,
+ onFinished: (Velocity) -> Unit
+ ) {
+ performFling(velocity = available.pixelsPerSecond.toFloat()) {
+ // since we go to the anchor with tween settling, consume all for the best UX
+ onFinished.invoke(available)
+ }
+ }
+
+ private fun Float.toOffset(): Offset = Offset(0f, this)
+
+ private fun Offset.toFloat(): Float = this.y
+ }
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Switch.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Switch.kt
index 9881050..c60a39c 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Switch.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Switch.kt
@@ -65,7 +65,7 @@
* [InteractionState] if you want to read the [InteractionState] and customize the appearance /
* behavior of this Switch in different [Interaction]s.
* @param colors [SwitchColors] that will be used to determine the color of the thumb and track
- * in different states. See [SwitchConstants.defaultColors].
+ * in different states. See [SwitchDefaults.colors].
*/
@Composable
@OptIn(ExperimentalMaterialApi::class)
@@ -75,7 +75,7 @@
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionState: InteractionState = remember { InteractionState() },
- colors: SwitchColors = SwitchConstants.defaultColors()
+ colors: SwitchColors = SwitchDefaults.colors()
) {
val minBound = 0f
val maxBound = with(AmbientDensity.current) { ThumbPathLength.toPx() }
@@ -117,7 +117,7 @@
/**
* Represents the colors used by a [Switch] in different states
*
- * See [SwitchConstants.defaultColors] for the default implementation that follows Material
+ * See [SwitchDefaults.colors] for the default implementation that follows Material
* specifications.
*/
@ExperimentalMaterialApi
@@ -208,6 +208,13 @@
/**
* Contains the default values used by [Switch]
*/
+@Deprecated(
+ "SwitchConstants has been replaced with SwitchDefaults",
+ ReplaceWith(
+ "SwitchDefaults",
+ "androidx.compose.material.SwitchDefaults"
+ )
+)
object SwitchConstants {
/**
* Creates a [SwitchColors] that represents the different colors used in a [Switch] in
@@ -228,6 +235,16 @@
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
+ @Deprecated(
+ "SwitchConstants has been replaced with SwitchDefaults",
+ ReplaceWith(
+ "SwitchDefaults.colors(checkedThumbColor, checkedTrackColor, checkedTrackAlpha, " +
+ "uncheckedThumbColor, uncheckedTrackColor, uncheckedTrackAlpha, " +
+ "disabledCheckedThumbColor, disabledCheckedTrackColor, " +
+ "disabledUncheckedThumbColor, disabledUncheckedTrackColor)",
+ "androidx.compose.material.SwitchDefaults"
+ )
+ )
fun defaultColors(
checkedThumbColor: Color = MaterialTheme.colors.secondaryVariant,
checkedTrackColor: Color = checkedThumbColor,
@@ -260,6 +277,60 @@
}
/**
+ * Contains the default values used by [Switch]
+ */
+object SwitchDefaults {
+ /**
+ * Creates a [SwitchColors] that represents the different colors used in a [Switch] in
+ * different states.
+ *
+ * @param checkedThumbColor the color used for the thumb when enabled and checked
+ * @param checkedTrackColor the color used for the track when enabled and checked
+ * @param checkedTrackAlpha the alpha applied to [checkedTrackColor] and
+ * [disabledCheckedTrackColor]
+ * @param uncheckedThumbColor the color used for the thumb when enabled and unchecked
+ * @param uncheckedTrackColor the color used for the track when enabled and unchecked
+ * @param uncheckedTrackAlpha the alpha applied to [uncheckedTrackColor] and
+ * [disabledUncheckedTrackColor]
+ * @param disabledCheckedThumbColor the color used for the thumb when disabled and checked
+ * @param disabledCheckedTrackColor the color used for the track when disabled and checked
+ * @param disabledUncheckedThumbColor the color used for the thumb when disabled and unchecked
+ * @param disabledUncheckedTrackColor the color used for the track when disabled and unchecked
+ */
+ @OptIn(ExperimentalMaterialApi::class)
+ @Composable
+ fun colors(
+ checkedThumbColor: Color = MaterialTheme.colors.secondaryVariant,
+ checkedTrackColor: Color = checkedThumbColor,
+ checkedTrackAlpha: Float = 0.54f,
+ uncheckedThumbColor: Color = MaterialTheme.colors.surface,
+ uncheckedTrackColor: Color = MaterialTheme.colors.onSurface,
+ uncheckedTrackAlpha: Float = 0.38f,
+ disabledCheckedThumbColor: Color = checkedThumbColor
+ .copy(alpha = ContentAlpha.disabled)
+ .compositeOver(MaterialTheme.colors.surface),
+ disabledCheckedTrackColor: Color = checkedTrackColor
+ .copy(alpha = ContentAlpha.disabled)
+ .compositeOver(MaterialTheme.colors.surface),
+ disabledUncheckedThumbColor: Color = uncheckedThumbColor
+ .copy(alpha = ContentAlpha.disabled)
+ .compositeOver(MaterialTheme.colors.surface),
+ disabledUncheckedTrackColor: Color = uncheckedTrackColor
+ .copy(alpha = ContentAlpha.disabled)
+ .compositeOver(MaterialTheme.colors.surface)
+ ): SwitchColors = DefaultSwitchColors(
+ checkedThumbColor = checkedThumbColor,
+ checkedTrackColor = checkedTrackColor.copy(alpha = checkedTrackAlpha),
+ uncheckedThumbColor = uncheckedThumbColor,
+ uncheckedTrackColor = uncheckedTrackColor.copy(alpha = uncheckedTrackAlpha),
+ disabledCheckedThumbColor = disabledCheckedThumbColor,
+ disabledCheckedTrackColor = disabledCheckedTrackColor.copy(alpha = checkedTrackAlpha),
+ disabledUncheckedThumbColor = disabledUncheckedThumbColor,
+ disabledUncheckedTrackColor = disabledUncheckedTrackColor.copy(alpha = uncheckedTrackAlpha)
+ )
+}
+
+/**
* Default [SwitchColors] implementation.
*/
@OptIn(ExperimentalMaterialApi::class)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt
index 175501d..8d4ed29 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt
@@ -165,6 +165,13 @@
/**
* Contains default values used by tabs from the Material specification.
*/
+@Deprecated(
+ "TabConstants has been replaced with TabDefaults",
+ ReplaceWith(
+ "TabDefaults",
+ "androidx.compose.material.TabDefaults"
+ )
+)
object TabConstants {
/**
* Default [Divider], which will be positioned at the bottom of the [TabRow], underneath the
@@ -254,6 +261,98 @@
val DefaultScrollableTabRowPadding = 52.dp
}
+/**
+ * Contains default values used by tabs from the Material specification.
+ */
+object TabDefaults {
+ /**
+ * Default [Divider], which will be positioned at the bottom of the [TabRow], underneath the
+ * indicator.
+ *
+ * @param modifier modifier for the divider's layout
+ * @param thickness thickness of the divider
+ * @param color color of the divider
+ */
+ @Composable
+ fun Divider(
+ modifier: Modifier = Modifier,
+ thickness: Dp = DividerThickness,
+ color: Color = AmbientContentColor.current.copy(alpha = DividerOpacity)
+ ) {
+ androidx.compose.material.Divider(modifier = modifier, thickness = thickness, color = color)
+ }
+
+ /**
+ * Default indicator, which will be positioned at the bottom of the [TabRow], on top of the
+ * divider.
+ *
+ * @param modifier modifier for the indicator's layout
+ * @param height height of the indicator
+ * @param color color of the indicator
+ */
+ @Composable
+ fun Indicator(
+ modifier: Modifier = Modifier,
+ height: Dp = IndicatorHeight,
+ color: Color = AmbientContentColor.current
+ ) {
+ Box(
+ modifier
+ .fillMaxWidth()
+ .preferredHeight(height)
+ .background(color = color)
+ )
+ }
+
+ /**
+ * [Modifier] that takes up all the available width inside the [TabRow], and then animates
+ * the offset of the indicator it is applied to, depending on the [currentTabPosition].
+ *
+ * @param currentTabPosition [TabPosition] of the currently selected tab. This is used to
+ * calculate the offset of the indicator this modifier is applied to, as well as its width.
+ */
+ fun Modifier.tabIndicatorOffset(
+ currentTabPosition: TabPosition
+ ): Modifier = composed(
+ inspectorInfo = debugInspectorInfo {
+ name = "tabIndicatorOffset"
+ value = currentTabPosition
+ }
+ ) {
+ // TODO: should we animate the width of the indicator as it moves between tabs of different
+ // sizes inside a scrollable tab row?
+ val currentTabWidth = currentTabPosition.width
+ val indicatorOffset = animate(
+ target = currentTabPosition.left,
+ animSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing)
+ )
+ fillMaxWidth()
+ .wrapContentSize(Alignment.BottomStart)
+ .offset(x = indicatorOffset)
+ .preferredWidth(currentTabWidth)
+ }
+
+ /**
+ * Default opacity for the color of [Divider]
+ */
+ const val DividerOpacity = 0.12f
+
+ /**
+ * Default thickness for [Divider]
+ */
+ val DividerThickness = 1.dp
+
+ /**
+ * Default height for [Indicator]
+ */
+ val IndicatorHeight = 2.dp
+
+ /**
+ * The default padding from the starting edge before a tab in a [ScrollableTabRow].
+ */
+ val ScrollableTabRowPadding = 52.dp
+}
+
private val TabTintColor = ColorPropKey()
/**
@@ -396,7 +495,7 @@
// Total offset between the last text baseline and the bottom of the Tab layout
val totalOffset = with(density) {
- baselineOffset.toIntPx() + TabConstants.DefaultIndicatorHeight.toIntPx()
+ baselineOffset.toIntPx() + TabDefaults.IndicatorHeight.toIntPx()
}
val textPlaceableY = tabHeight - lastBaseline - totalOffset
@@ -425,7 +524,7 @@
// Total offset between the last text baseline and the bottom of the Tab layout
val textOffset = with(density) {
- baselineOffset.toIntPx() + TabConstants.DefaultIndicatorHeight.toIntPx()
+ baselineOffset.toIntPx() + TabDefaults.IndicatorHeight.toIntPx()
}
// How much space there is between the top of the icon (essentially the top of this layout)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TabRow.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TabRow.kt
index 4486035..04c42c9 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TabRow.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TabRow.kt
@@ -24,7 +24,7 @@
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
-import androidx.compose.material.TabConstants.defaultTabIndicatorOffset
+import androidx.compose.material.TabDefaults.tabIndicatorOffset
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
@@ -69,7 +69,7 @@
*
* @sample androidx.compose.material.samples.FancyIndicator
*
- * We can reuse [TabConstants.defaultTabIndicatorOffset] and just provide this indicator,
+ * We can reuse [TabDefaults.tabIndicatorOffset] and just provide this indicator,
* as we aren't changing how the size and position of the indicator changes between tabs:
*
* @sample androidx.compose.material.samples.FancyIndicatorTabs
@@ -96,9 +96,9 @@
* Defaults to either the matching `onFoo` color for [backgroundColor], or if [backgroundColor] is
* not a color from the theme, this will keep the same value set above this TabRow.
* @param indicator the indicator that represents which tab is currently selected. By default this
- * will be a [TabConstants.DefaultIndicator], using a [TabConstants.defaultTabIndicatorOffset]
+ * will be a [TabDefaults.Indicator], using a [TabDefaults.tabIndicatorOffset]
* modifier to animate its position. Note that this indicator will be forced to fill up the
- * entire TabRow, so you should use [TabConstants.defaultTabIndicatorOffset] or similar to
+ * entire TabRow, so you should use [TabDefaults.tabIndicatorOffset] or similar to
* animate the actual drawn indicator inside this space, and provide an offset from the start.
* @param divider the divider displayed at the bottom of the TabRow. This provides a layer of
* separation between the TabRow and the content displayed underneath.
@@ -113,12 +113,12 @@
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor),
indicator: @Composable (tabPositions: List<TabPosition>) -> Unit = { tabPositions ->
- TabConstants.DefaultIndicator(
- Modifier.defaultTabIndicatorOffset(tabPositions[selectedTabIndex])
+ TabDefaults.Indicator(
+ Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
)
},
divider: @Composable () -> Unit = {
- TabConstants.DefaultDivider()
+ TabDefaults.Divider()
},
tabs: @Composable () -> Unit
) {
@@ -176,9 +176,9 @@
* the tabs inside the ScrollableTabRow. This padding helps inform the user that this tab row can
* be scrolled, unlike a [TabRow].
* @param indicator the indicator that represents which tab is currently selected. By default this
- * will be a [TabConstants.DefaultIndicator], using a [TabConstants.defaultTabIndicatorOffset]
+ * will be a [TabDefaults.Indicator], using a [TabDefaults.tabIndicatorOffset]
* modifier to animate its position. Note that this indicator will be forced to fill up the
- * entire ScrollableTabRow, so you should use [TabConstants.defaultTabIndicatorOffset] or similar to
+ * entire ScrollableTabRow, so you should use [TabDefaults.tabIndicatorOffset] or similar to
* animate the actual drawn indicator inside this space, and provide an offset from the start.
* @param divider the divider displayed at the bottom of the ScrollableTabRow. This provides a layer
* of separation between the ScrollableTabRow and the content displayed underneath.
@@ -192,14 +192,14 @@
modifier: Modifier = Modifier,
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor),
- edgePadding: Dp = TabConstants.DefaultScrollableTabRowPadding,
+ edgePadding: Dp = TabDefaults.ScrollableTabRowPadding,
indicator: @Composable (tabPositions: List<TabPosition>) -> Unit = { tabPositions ->
- TabConstants.DefaultIndicator(
- Modifier.defaultTabIndicatorOffset(tabPositions[selectedTabIndex])
+ TabDefaults.Indicator(
+ Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
)
},
divider: @Composable () -> Unit = {
- TabConstants.DefaultDivider()
+ TabDefaults.Divider()
},
tabs: @Composable () -> Unit
) {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldImpl.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldImpl.kt
index 1bacb70..e66dad5 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldImpl.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldImpl.kt
@@ -39,7 +39,6 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focusRequester
import androidx.compose.ui.graphics.Color
@@ -73,10 +72,7 @@
* Implementation of the [TextField] and [OutlinedTextField]
*/
@Composable
-@OptIn(
- ExperimentalFocus::class,
- ExperimentalFoundationApi::class
-)
+@OptIn(ExperimentalFoundationApi::class)
internal fun TextFieldImpl(
type: TextFieldType,
value: TextFieldValue,
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetector.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetector.kt
index dba48a2..704b120 100644
--- a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetector.kt
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetector.kt
@@ -143,7 +143,7 @@
"Primary composable lambda parameter not named `content`",
"Composable functions with only one composable lambda parameter should use the name " +
"`content` for the parameter.",
- Category.CORRECTNESS, 3, Severity.WARNING,
+ Category.CORRECTNESS, 3, Severity.IGNORE,
Implementation(
ComposableLambdaParameterDetector::class.java,
EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
@@ -156,7 +156,7 @@
"Composable functions with only one composable lambda parameter should place the " +
"parameter at the end of the parameter list, so it can be used as a trailing " +
"lambda.",
- Category.CORRECTNESS, 3, Severity.WARNING,
+ Category.CORRECTNESS, 3, Severity.IGNORE,
Implementation(
ComposableLambdaParameterDetector::class.java,
EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RememberDetector.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RememberDetector.kt
new file mode 100644
index 0000000..d05d0b1
--- /dev/null
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RememberDetector.kt
@@ -0,0 +1,77 @@
+/*
+ * 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("UnstableApiUsage")
+
+package androidx.compose.runtime.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiJavaFile
+import com.intellij.psi.PsiType
+import org.jetbrains.uast.UCallExpression
+import java.util.EnumSet
+
+/**
+ * [Detector] that checks `remember` calls to make sure they are not returning [Unit].
+ */
+class RememberDetector : Detector(), SourceCodeScanner {
+ override fun getApplicableUastTypes() = listOf(UCallExpression::class.java)
+
+ override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
+ override fun visitCallExpression(node: UCallExpression) {
+ val call = node.resolve() ?: return
+ if (call.name == RememberShortName &&
+ (call.containingFile as? PsiJavaFile)?.packageName == RuntimePackageName
+ ) {
+ if (node.getExpressionType() == PsiType.VOID) {
+ context.report(
+ RememberReturnType,
+ node,
+ context.getNameLocation(node),
+ "`remember` calls must not return `Unit`"
+ )
+ }
+ }
+ }
+ }
+
+ companion object {
+ val RememberReturnType = Issue.create(
+ "RememberReturnType",
+ "`remember` calls must not return `Unit`",
+ "A call to `remember` that returns `Unit` is always an error. This typically happens " +
+ "when using `remember` to mutate variables on an object. `remember` is executed " +
+ "during the composition, which means that if the composition fails or is " +
+ "happening on a separate thread, the mutated variables may not reflect the true " +
+ "state of the composition. Instead, use `SideEffect` to make deferred changes " +
+ "once the composition succeeds, or mutate `MutableState` backed variables " +
+ "directly, as these will handle composition failure for you.",
+ Category.CORRECTNESS, 3, Severity.ERROR,
+ Implementation(
+ RememberDetector::class.java,
+ EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
+ )
+ )
+ }
+}
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt
index 7ab6018..6d5dcad 100644
--- a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt
@@ -30,6 +30,7 @@
AmbientNamingDetector.AmbientNaming,
ComposableLambdaParameterDetector.ComposableLambdaParameterNaming,
ComposableLambdaParameterDetector.ComposableLambdaParameterPosition,
- ComposableNamingDetector.ComposableNaming
+ ComposableNamingDetector.ComposableNaming,
+ RememberDetector.RememberReturnType
)
}
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/Utils.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/Utils.kt
index 33d78b0..6d8f4d6 100644
--- a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/Utils.kt
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/Utils.kt
@@ -25,5 +25,9 @@
@Suppress("DEPRECATION")
val UMethod.isComposable get() = annotations.any { it.qualifiedName == ComposableFqn }
-const val ComposableFqn = "androidx.compose.runtime.Composable"
-val ComposableShortName = ComposableFqn.split(".").last()
\ No newline at end of file
+const val RuntimePackageName = "androidx.compose.runtime"
+
+const val ComposableFqn = "$RuntimePackageName.Composable"
+val ComposableShortName = ComposableFqn.split(".").last()
+
+const val RememberShortName = "remember"
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
new file mode 100644
index 0000000..06f8db1
--- /dev/null
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/RememberDetectorTest.kt
@@ -0,0 +1,339 @@
+/*
+ * 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("UnstableApiUsage")
+
+package androidx.compose.runtime.lint
+
+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.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/* ktlint-disable max-line-length */
+@RunWith(JUnit4::class)
+
+/**
+ * Test for [RememberDetector].
+ */
+class RememberDetectorTest : LintDetectorTest() {
+ override fun getDetector(): Detector = RememberDetector()
+
+ override fun getIssues(): MutableList<Issue> =
+ mutableListOf(RememberDetector.RememberReturnType)
+
+ @Test
+ fun returnsUnit() {
+ lint().files(
+ kotlin(
+ """
+ package androidx.compose.runtime.foo
+
+ import androidx.compose.runtime.Composable
+ import androidx.compose.runtime.remember
+
+ class FooState {
+ fun update(new: Int) {}
+ }
+
+ @Composable
+ fun Test() {
+ val state = remember { FooState() }
+ remember {
+ state.update(5)
+ }
+ val unit = remember {
+ state.update(5)
+ }
+ }
+
+ @Composable
+ fun Test(number: Int) {
+ val state = remember { FooState() }
+ remember(number) {
+ state.update(number)
+ }
+ val unit = remember(number) {
+ state.update(number)
+ }
+ }
+
+ @Composable
+ fun Test(number1: Int, number2: Int) {
+ val state = remember { FooState() }
+ remember(number1, number2) {
+ state.update(number)
+ }
+ val unit = remember(number1, number2) {
+ state.update(number)
+ }
+ }
+
+ @Composable
+ fun Test(number1: Int, number2: Int, number3: Int) {
+ val state = remember { FooState() }
+ remember(number1, number2, number3) {
+ state.update(number)
+ }
+ val unit = remember(number1, number2, number3) {
+ state.update(number)
+ }
+ }
+
+ @Composable
+ fun Test(number1: Int, number2: Int, number3: Int, flag: Boolean) {
+ val state = remember { FooState() }
+ remember(number1, number2, number3, flag) {
+ state.update(number)
+ }
+ val unit = remember(number1, number2, number3, flag) {
+ state.update(number)
+ }
+ }
+ """
+ ),
+ composableStub,
+ rememberStub
+ )
+ .run()
+ .expect(
+ """
+src/androidx/compose/runtime/foo/FooState.kt:14: Error: remember calls must not return Unit [RememberReturnType]
+ remember {
+ ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:17: Error: remember calls must not return Unit [RememberReturnType]
+ val unit = remember {
+ ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:25: Error: remember calls must not return Unit [RememberReturnType]
+ remember(number) {
+ ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:28: Error: remember calls must not return Unit [RememberReturnType]
+ val unit = remember(number) {
+ ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:36: Error: remember calls must not return Unit [RememberReturnType]
+ remember(number1, number2) {
+ ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:39: Error: remember calls must not return Unit [RememberReturnType]
+ val unit = remember(number1, number2) {
+ ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:47: Error: remember calls must not return Unit [RememberReturnType]
+ remember(number1, number2, number3) {
+ ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:50: Error: remember calls must not return Unit [RememberReturnType]
+ val unit = remember(number1, number2, number3) {
+ ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:58: Error: remember calls must not return Unit [RememberReturnType]
+ remember(number1, number2, number3, flag) {
+ ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:61: Error: remember calls must not return Unit [RememberReturnType]
+ val unit = remember(number1, number2, number3, flag) {
+ ~~~~~~~~
+10 errors, 0 warnings
+ """
+ )
+ }
+
+ @Test
+ fun returnsValue_explicitUnitType() {
+ lint().files(
+ kotlin(
+ """
+ package androidx.compose.runtime.foo
+
+ import androidx.compose.runtime.Composable
+ import androidx.compose.runtime.remember
+
+ class FooState {
+ fun update(new: Int): Boolean = true
+ }
+
+ @Composable
+ fun Test() {
+ val state = remember { FooState() }
+ remember<Unit> {
+ state.update(5)
+ }
+ val result = remember<Unit> {
+ state.update(5)
+ }
+ }
+
+ @Composable
+ fun Test(number: Int) {
+ val state = remember { FooState() }
+ remember<Unit>(number) {
+ state.update(number)
+ }
+ val result = remember<Unit>(number) {
+ state.update(number)
+ }
+ }
+
+ @Composable
+ fun Test(number1: Int, number2: Int) {
+ val state = remember { FooState() }
+ remember<Unit>(number1, number2) {
+ state.update(number)
+ }
+ val result = remember<Unit>(number1, number2) {
+ state.update(number)
+ }
+ }
+
+ @Composable
+ fun Test(number1: Int, number2: Int, number3: Int) {
+ val state = remember { FooState() }
+ remember<Unit>(number1, number2, number3) {
+ state.update(number)
+ }
+ val result = remember<Unit>(number1, number2, number3) {
+ state.update(number)
+ }
+ }
+
+ @Composable
+ fun Test(number1: Int, number2: Int, number3: Int, flag: Boolean) {
+ val state = remember { FooState() }
+ remember<Unit>(number1, number2, number3, flag) {
+ state.update(number)
+ }
+ val result = remember<Unit>(number1, number2, number3, flag) {
+ state.update(number)
+ }
+ }
+ """
+ ),
+ composableStub,
+ rememberStub
+ )
+ .run()
+ .expect(
+ """
+src/androidx/compose/runtime/foo/FooState.kt:14: Error: remember calls must not return Unit [RememberReturnType]
+ remember<Unit> {
+ ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:17: Error: remember calls must not return Unit [RememberReturnType]
+ val result = remember<Unit> {
+ ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:25: Error: remember calls must not return Unit [RememberReturnType]
+ remember<Unit>(number) {
+ ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:28: Error: remember calls must not return Unit [RememberReturnType]
+ val result = remember<Unit>(number) {
+ ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:36: Error: remember calls must not return Unit [RememberReturnType]
+ remember<Unit>(number1, number2) {
+ ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:39: Error: remember calls must not return Unit [RememberReturnType]
+ val result = remember<Unit>(number1, number2) {
+ ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:47: Error: remember calls must not return Unit [RememberReturnType]
+ remember<Unit>(number1, number2, number3) {
+ ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:50: Error: remember calls must not return Unit [RememberReturnType]
+ val result = remember<Unit>(number1, number2, number3) {
+ ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:58: Error: remember calls must not return Unit [RememberReturnType]
+ remember<Unit>(number1, number2, number3, flag) {
+ ~~~~~~~~
+src/androidx/compose/runtime/foo/FooState.kt:61: Error: remember calls must not return Unit [RememberReturnType]
+ val result = remember<Unit>(number1, number2, number3, flag) {
+ ~~~~~~~~
+10 errors, 0 warnings
+ """
+ )
+ }
+
+ @Test
+ fun noErrors() {
+ lint().files(
+ kotlin(
+ """
+ package androidx.compose.runtime.foo
+
+ import androidx.compose.runtime.Composable
+ import androidx.compose.runtime.remember
+
+ class FooState {
+ fun update(new: Int): Boolean = true
+ }
+
+ @Composable
+ fun Test() {
+ val state = remember { FooState() }
+ remember {
+ state.update(5)
+ }
+ val result = remember {
+ state.update(5)
+ }
+ }
+
+ @Composable
+ fun Test(number: Int) {
+ val state = remember { FooState() }
+ remember(number) {
+ state.update(number)
+ }
+ val result = remember(number) {
+ state.update(number)
+ }
+ }
+
+ @Composable
+ fun Test(number1: Int, number2: Int) {
+ val state = remember { FooState() }
+ remember(number1, number2) {
+ state.update(number)
+ }
+ val result = remember(number1, number2) {
+ state.update(number)
+ }
+ }
+
+ @Composable
+ fun Test(number1: Int, number2: Int, number3: Int) {
+ val state = remember { FooState() }
+ remember(number1, number2, number3) {
+ state.update(number)
+ }
+ val result = remember(number1, number2, number3) {
+ state.update(number)
+ }
+ }
+
+ @Composable
+ fun Test(number1: Int, number2: Int, number3: Int, flag: Boolean) {
+ val state = remember { FooState() }
+ remember(number1, number2, number3, flag) {
+ state.update(number)
+ }
+ val result = remember(number1, number2, number3, flag) {
+ state.update(number)
+ }
+ }
+ """
+ ),
+ composableStub,
+ rememberStub
+ )
+ .run()
+ .expectClean()
+ }
+}
+/* ktlint-enable max-line-length */
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/Stubs.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/Stubs.kt
index 92428e8..9c086d2 100644
--- a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/Stubs.kt
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/Stubs.kt
@@ -34,4 +34,42 @@
)
annotation class Composable
"""
-)
\ No newline at end of file
+)
+
+val rememberStub: LintDetectorTest.TestFile = LintDetectorTest.kotlin(
+"""
+ package androidx.compose.runtime
+
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ inline fun <T> remember(calculation: () -> T): T = calculation()
+
+ @Composable
+ inline fun <T, V1> remember(
+ v1: V1,
+ calculation: () -> T
+ ): T = calculation()
+
+ @Composable
+ inline fun <T, V1, V2> remember(
+ v1: V1,
+ v2: V2,
+ calculation: () -> T
+ ): T = calculation()
+
+ @Composable
+ inline fun <T, V1, V2, V3> remember(
+ v1: V1,
+ v2: V2,
+ v3: V3,
+ calculation: () -> T
+ ): T = calculation()
+
+ @Composable
+ inline fun <V> remember(
+ vararg inputs: Any?,
+ calculation: () -> V
+ ): V = calculation()
+ """
+)
diff --git a/compose/runtime/runtime-saved-instance-state/src/commonMain/kotlin/androidx/compose/runtime/savedinstancestate/RestorableStateHolder.kt b/compose/runtime/runtime-saved-instance-state/src/commonMain/kotlin/androidx/compose/runtime/savedinstancestate/RestorableStateHolder.kt
index ad90774..f72bd7f 100644
--- a/compose/runtime/runtime-saved-instance-state/src/commonMain/kotlin/androidx/compose/runtime/savedinstancestate/RestorableStateHolder.kt
+++ b/compose/runtime/runtime-saved-instance-state/src/commonMain/kotlin/androidx/compose/runtime/savedinstancestate/RestorableStateHolder.kt
@@ -104,7 +104,7 @@
content = content
)
onActive {
- require(key !in registryHolders)
+ require(key !in registryHolders) { "Key $key was used multiple times " }
savedStates -= key
registryHolders[key] = registryHolder
onDispose {
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index 61b92204..34986e5 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.compose.runtime {
- @androidx.compose.runtime.ExperimentalComposeApi public abstract class AbstractApplier<T> implements androidx.compose.runtime.Applier<T> {
+ public abstract class AbstractApplier<T> implements androidx.compose.runtime.Applier<T> {
ctor public AbstractApplier(T? root);
method public final void clear();
method public void down(T? node);
@@ -24,8 +24,8 @@
}
@androidx.compose.runtime.Stable public abstract sealed class Ambient<T> {
- method public final inline T! getCurrent();
- property public final inline T! current;
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final inline T! getCurrent();
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final inline T! current;
}
public final class AmbientKt {
@@ -41,11 +41,12 @@
property public final boolean valid;
}
- @androidx.compose.runtime.ExperimentalComposeApi public interface Applier<N> {
+ public interface Applier<N> {
method public void clear();
method public void down(N? node);
method public N! getCurrent();
- method public void insert(int index, N? instance);
+ method public void insertBottomUp(int index, N? instance);
+ method public void insertTopDown(int index, N? instance);
method public void move(int from, int to, int count);
method public default void onBeginChanges();
method public default void onEndChanges();
@@ -61,10 +62,10 @@
method public void onDispose(kotlin.jvm.functions.Function0<kotlin.Unit> callback);
}
- @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface Composable {
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface Composable {
}
- @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ComposableContract {
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ComposableContract {
method public abstract boolean preventCapture() default false;
method public abstract boolean readonly() default false;
method public abstract boolean restartable() default true;
@@ -97,6 +98,7 @@
method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(double value);
method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(int value);
method @androidx.compose.runtime.InternalComposeApi public void collectKeySourceInformation();
+ method @androidx.compose.runtime.InternalComposeApi public void collectParameterInformation();
method @androidx.compose.runtime.InternalComposeApi public void composeInitial(kotlin.jvm.functions.Function0<kotlin.Unit> block);
method @androidx.compose.runtime.ComposeCompilerApi public void endDefaults();
method @androidx.compose.runtime.ComposeCompilerApi public void endMovableGroup();
@@ -129,7 +131,7 @@
}
public final class ComposerKt {
- method public static androidx.compose.runtime.Composer<?> getCurrentComposer();
+ method @androidx.compose.runtime.Composable public static androidx.compose.runtime.Composer<?> getCurrentComposer();
}
public interface Composition {
@@ -168,7 +170,7 @@
public final class EffectsKt {
method @androidx.compose.runtime.Composable public static androidx.compose.runtime.CompositionReference compositionReference();
- method public static kotlin.jvm.functions.Function0<kotlin.Unit> getInvalidate();
+ method @androidx.compose.runtime.Composable public static kotlin.jvm.functions.Function0<kotlin.Unit> getInvalidate();
method @androidx.compose.runtime.Composable public static void onActive(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> callback);
method @androidx.compose.runtime.Composable public static inline void onCommit(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> callback);
method @androidx.compose.runtime.Composable public static <V1> void onCommit(V1? v1, kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> callback);
@@ -665,6 +667,7 @@
}
public final class LiveLiteralKt {
+ method @androidx.compose.runtime.InternalComposeApi public static void enableLiveLiterals();
method public static boolean isLiveLiteralsEnabled();
method @androidx.compose.runtime.InternalComposeApi public static <T> androidx.compose.runtime.State<T> liveLiteral(String key, T? value);
method @androidx.compose.runtime.InternalComposeApi public static void updateLiveLiteralValue(String key, Object? value);
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index 61b92204..34986e5 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.compose.runtime {
- @androidx.compose.runtime.ExperimentalComposeApi public abstract class AbstractApplier<T> implements androidx.compose.runtime.Applier<T> {
+ public abstract class AbstractApplier<T> implements androidx.compose.runtime.Applier<T> {
ctor public AbstractApplier(T? root);
method public final void clear();
method public void down(T? node);
@@ -24,8 +24,8 @@
}
@androidx.compose.runtime.Stable public abstract sealed class Ambient<T> {
- method public final inline T! getCurrent();
- property public final inline T! current;
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final inline T! getCurrent();
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final inline T! current;
}
public final class AmbientKt {
@@ -41,11 +41,12 @@
property public final boolean valid;
}
- @androidx.compose.runtime.ExperimentalComposeApi public interface Applier<N> {
+ public interface Applier<N> {
method public void clear();
method public void down(N? node);
method public N! getCurrent();
- method public void insert(int index, N? instance);
+ method public void insertBottomUp(int index, N? instance);
+ method public void insertTopDown(int index, N? instance);
method public void move(int from, int to, int count);
method public default void onBeginChanges();
method public default void onEndChanges();
@@ -61,10 +62,10 @@
method public void onDispose(kotlin.jvm.functions.Function0<kotlin.Unit> callback);
}
- @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface Composable {
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface Composable {
}
- @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ComposableContract {
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ComposableContract {
method public abstract boolean preventCapture() default false;
method public abstract boolean readonly() default false;
method public abstract boolean restartable() default true;
@@ -97,6 +98,7 @@
method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(double value);
method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(int value);
method @androidx.compose.runtime.InternalComposeApi public void collectKeySourceInformation();
+ method @androidx.compose.runtime.InternalComposeApi public void collectParameterInformation();
method @androidx.compose.runtime.InternalComposeApi public void composeInitial(kotlin.jvm.functions.Function0<kotlin.Unit> block);
method @androidx.compose.runtime.ComposeCompilerApi public void endDefaults();
method @androidx.compose.runtime.ComposeCompilerApi public void endMovableGroup();
@@ -129,7 +131,7 @@
}
public final class ComposerKt {
- method public static androidx.compose.runtime.Composer<?> getCurrentComposer();
+ method @androidx.compose.runtime.Composable public static androidx.compose.runtime.Composer<?> getCurrentComposer();
}
public interface Composition {
@@ -168,7 +170,7 @@
public final class EffectsKt {
method @androidx.compose.runtime.Composable public static androidx.compose.runtime.CompositionReference compositionReference();
- method public static kotlin.jvm.functions.Function0<kotlin.Unit> getInvalidate();
+ method @androidx.compose.runtime.Composable public static kotlin.jvm.functions.Function0<kotlin.Unit> getInvalidate();
method @androidx.compose.runtime.Composable public static void onActive(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> callback);
method @androidx.compose.runtime.Composable public static inline void onCommit(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> callback);
method @androidx.compose.runtime.Composable public static <V1> void onCommit(V1? v1, kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> callback);
@@ -665,6 +667,7 @@
}
public final class LiveLiteralKt {
+ method @androidx.compose.runtime.InternalComposeApi public static void enableLiveLiterals();
method public static boolean isLiveLiteralsEnabled();
method @androidx.compose.runtime.InternalComposeApi public static <T> androidx.compose.runtime.State<T> liveLiteral(String key, T? value);
method @androidx.compose.runtime.InternalComposeApi public static void updateLiveLiteralValue(String key, Object? value);
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index a649fe2..c0658ef 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.compose.runtime {
- @androidx.compose.runtime.ExperimentalComposeApi public abstract class AbstractApplier<T> implements androidx.compose.runtime.Applier<T> {
+ public abstract class AbstractApplier<T> implements androidx.compose.runtime.Applier<T> {
ctor public AbstractApplier(T? root);
method public final void clear();
method public void down(T? node);
@@ -24,8 +24,8 @@
}
@androidx.compose.runtime.Stable public abstract sealed class Ambient<T> {
- method public final inline T! getCurrent();
- property public final inline T! current;
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final inline T! getCurrent();
+ property @androidx.compose.runtime.Composable @androidx.compose.runtime.ComposableContract(readonly=true) public final inline T! current;
}
public final class AmbientKt {
@@ -41,11 +41,12 @@
property public final boolean valid;
}
- @androidx.compose.runtime.ExperimentalComposeApi public interface Applier<N> {
+ public interface Applier<N> {
method public void clear();
method public void down(N? node);
method public N! getCurrent();
- method public void insert(int index, N? instance);
+ method public void insertBottomUp(int index, N? instance);
+ method public void insertTopDown(int index, N? instance);
method public void move(int from, int to, int count);
method public default void onBeginChanges();
method public default void onEndChanges();
@@ -61,10 +62,10 @@
method public void onDispose(kotlin.jvm.functions.Function0<kotlin.Unit> callback);
}
- @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface Composable {
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface Composable {
}
- @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ComposableContract {
+ @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ComposableContract {
method public abstract boolean preventCapture() default false;
method public abstract boolean readonly() default false;
method public abstract boolean restartable() default true;
@@ -97,6 +98,7 @@
method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(double value);
method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(int value);
method @androidx.compose.runtime.InternalComposeApi public void collectKeySourceInformation();
+ method @androidx.compose.runtime.InternalComposeApi public void collectParameterInformation();
method @androidx.compose.runtime.InternalComposeApi public void composeInitial(kotlin.jvm.functions.Function0<kotlin.Unit> block);
method @kotlin.PublishedApi internal <T> T! consume(androidx.compose.runtime.Ambient<T> key);
method @kotlin.PublishedApi internal void emitNode(Object? node);
@@ -137,7 +139,7 @@
}
public final class ComposerKt {
- method public static androidx.compose.runtime.Composer<?> getCurrentComposer();
+ method @androidx.compose.runtime.Composable public static androidx.compose.runtime.Composer<?> getCurrentComposer();
field @kotlin.PublishedApi internal static final androidx.compose.runtime.OpaqueKey ambientMap;
field @kotlin.PublishedApi internal static final int ambientMapKey = 202; // 0xca
field @kotlin.PublishedApi internal static final androidx.compose.runtime.OpaqueKey invocation;
@@ -194,7 +196,7 @@
public final class EffectsKt {
method @androidx.compose.runtime.Composable public static androidx.compose.runtime.CompositionReference compositionReference();
- method public static kotlin.jvm.functions.Function0<kotlin.Unit> getInvalidate();
+ method @androidx.compose.runtime.Composable public static kotlin.jvm.functions.Function0<kotlin.Unit> getInvalidate();
method @androidx.compose.runtime.Composable public static void onActive(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> callback);
method @androidx.compose.runtime.Composable public static inline void onCommit(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> callback);
method @androidx.compose.runtime.Composable public static <V1> void onCommit(V1? v1, kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> callback);
@@ -703,6 +705,7 @@
}
public final class LiveLiteralKt {
+ method @androidx.compose.runtime.InternalComposeApi public static void enableLiveLiterals();
method public static boolean isLiveLiteralsEnabled();
method @androidx.compose.runtime.InternalComposeApi public static <T> androidx.compose.runtime.State<T> liveLiteral(String key, T? value);
method @androidx.compose.runtime.InternalComposeApi public static void updateLiveLiteralValue(String key, Object? value);
diff --git a/compose/runtime/runtime/build.gradle b/compose/runtime/runtime/build.gradle
index 5a834fb..ab49bf9 100644
--- a/compose/runtime/runtime/build.gradle
+++ b/compose/runtime/runtime/build.gradle
@@ -51,6 +51,7 @@
testImplementation KOTLIN_TEST_JUNIT
testImplementation(JUNIT)
testImplementation(ROBOLECTRIC)
+ testImplementation(KOTLIN_COROUTINES_TEST)
androidTestImplementation KOTLIN_TEST_JUNIT
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
@@ -102,6 +103,7 @@
commonTest.dependencies {
implementation kotlin("test-junit")
+ implementation(KOTLIN_COROUTINES_TEST)
}
androidAndroidTest.dependencies {
implementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmarkBase.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmarkBase.kt
index b044cc9..b6ec566 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmarkBase.kt
+++ b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmarkBase.kt
@@ -16,9 +16,6 @@
package androidx.compose.runtime.benchmark
-import android.app.Activity
-import android.view.View
-import android.view.ViewGroup
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.compose.runtime.Composable
@@ -32,7 +29,6 @@
import androidx.compose.runtime.snapshots.SnapshotReadObserver
import androidx.compose.runtime.snapshots.SnapshotWriteObserver
import androidx.compose.runtime.snapshots.takeMutableSnapshot
-import androidx.compose.ui.platform.AndroidOwner
import androidx.compose.ui.platform.setContent
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.coroutineScope
@@ -189,25 +185,3 @@
updateModelCb = block
}
}
-
-// TODO(chuckj): Consider refacgtoring to use AndroidTestCaseRunner from UI
-// This code is copied from AndroidTestCaseRunner.kt
-private fun findComposeView(activity: Activity): AndroidOwner? {
- return findComposeView(activity.findViewById(android.R.id.content) as ViewGroup)
-}
-
-private fun findComposeView(view: View): AndroidOwner? {
- if (view is AndroidOwner) {
- return view
- }
-
- if (view is ViewGroup) {
- for (i in 0 until view.childCount) {
- val composeView = findComposeView(view.getChildAt(i))
- if (composeView != null) {
- return composeView
- }
- }
- }
- return null
-}
\ No newline at end of file
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/AmbientTests.kt b/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/AmbientTests.kt
index b1b6b8f..d932950 100644
--- a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/AmbientTests.kt
+++ b/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/AmbientTests.kt
@@ -17,9 +17,9 @@
@file:OptIn(ExperimentalComposeApi::class)
package androidx.compose.runtime
+import android.view.View
import android.widget.TextView
-import androidx.compose.ui.node.LayoutNode
-import androidx.compose.ui.platform.subcomposeInto
+import androidx.compose.ui.node.UiApplier
import androidx.compose.ui.viewinterop.emitView
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -496,17 +496,20 @@
}
@Composable fun deferredSubCompose(block: @Composable () -> Unit): () -> Unit {
- val container = remember { LayoutNode() }
+ val container = remember { View(activity) }
val ref = Ref<CompositionReference>()
narrowInvalidateForReference(ref = ref)
return {
@OptIn(ExperimentalComposeApi::class)
// TODO(b/150390669): Review use of @ComposableContract(tracked = false)
- subcomposeInto(
+ compositionFor(
container,
+ UiApplier(container),
ref.value
- ) @ComposableContract(tracked = false) {
- block()
+ ).apply {
+ setContent @ComposableContract(tracked = false) {
+ block()
+ }
}
}
}
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/BaseComposeTest.kt b/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/BaseComposeTest.kt
index 4ccd97f..51a66a2 100644
--- a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/BaseComposeTest.kt
+++ b/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/BaseComposeTest.kt
@@ -21,13 +21,13 @@
import android.os.Bundle
import android.os.Looper
import android.view.Choreographer
+import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.node.UiApplier
import androidx.compose.ui.platform.AmbientContext
import androidx.compose.ui.platform.setViewContent
-import androidx.compose.ui.platform.subcomposeInto
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.test.assertTrue
@@ -116,15 +116,18 @@
@Composable
fun subCompose(block: @Composable () -> Unit) {
- val container = remember { LayoutNode() }
+ val container = remember { View(activity) }
val reference = compositionReference()
// TODO(b/150390669): Review use of @ComposableContract(tracked = false)
@OptIn(ExperimentalComposeApi::class)
- subcomposeInto(
+ compositionFor(
container,
+ UiApplier(container),
reference
- ) @ComposableContract(tracked = false) {
- block()
+ ).apply {
+ setContent @ComposableContract(tracked = false) {
+ block()
+ }
}
}
}
diff --git a/compose/runtime/runtime/lint-baseline.xml b/compose/runtime/runtime/lint-baseline.xml
deleted file mode 100644
index 76c954a..0000000
--- a/compose/runtime/runtime/lint-baseline.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-alpha15" client="gradle" variant="debug" version="4.2.0-alpha15">
-
- <issue
- id="UnknownNullness"
- message="Should explicitly declare type here since implicit type does not specify nullness"
- errorLine1=" override fun removeAt(index: Int) = get(index).also { update { it.removeAt(index) } }"
- errorLine2=" ~~~~~~~~">
- <location
- file="src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt"
- line="91"
- column="18"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Should explicitly declare type here since implicit type does not specify nullness"
- errorLine1=" override fun set(index: Int, element: T) = get(index).also { update { it.set(index, element) } }"
- errorLine2=" ~~~">
- <location
- file="src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt"
- line="93"
- column="18"/>
- </issue>
-
-</issues>
diff --git a/compose/runtime/runtime/samples/src/main/java/androidx/compose/runtime/samples/CustomTreeCompositionSamples.kt b/compose/runtime/runtime/samples/src/main/java/androidx/compose/runtime/samples/CustomTreeCompositionSamples.kt
index 8eed09e..2d29baf 100644
--- a/compose/runtime/runtime/samples/src/main/java/androidx/compose/runtime/samples/CustomTreeCompositionSamples.kt
+++ b/compose/runtime/runtime/samples/src/main/java/androidx/compose/runtime/samples/CustomTreeCompositionSamples.kt
@@ -41,10 +41,14 @@
// We would implement an Applier class like the following, which would teach compose how to
// manage a tree of Nodes.
class NodeApplier(root: Node) : AbstractApplier<Node>(root) {
- override fun insert(index: Int, instance: Node) {
+ override fun insertTopDown(index: Int, instance: Node) {
current.children.add(index, instance)
}
+ override fun insertBottomUp(index: Int, instance: Node) {
+ // Ignored as the tree is built top-down.
+ }
+
override fun remove(index: Int, count: Int) {
current.children.remove(index, count)
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Ambient.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Ambient.kt
index 9bfa638..d01c5d3 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Ambient.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Ambient.kt
@@ -69,9 +69,10 @@
* @sample androidx.compose.runtime.samples.consumeAmbient
*/
@OptIn(ComposeCompilerApi::class)
- @ComposableContract(readonly = true)
- @Composable
- inline val current: T get() = currentComposer.consume(this)
+ inline val current: T
+ @ComposableContract(readonly = true)
+ @Composable
+ get() = currentComposer.consume(this)
}
/**
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
index 1f9410b..2d1be12 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
@@ -30,7 +30,6 @@
* @see Composer
* @see emit
*/
-@ExperimentalComposeApi
interface Applier<N> {
/**
* The node that operations will be applied on at any given time. It is expected that the
@@ -65,9 +64,98 @@
fun up()
/**
- * Indicates that [instance] should be inserted as a child to [current] at [index]
+ * Indicates that [instance] should be inserted as a child to [current] at [index]. An applier
+ * should insert the node into the tree either in [insertTopDown] or [insertBottomUp], not both.
+ *
+ * The [insertTopDown] method is called before the children of [instance] have been created and
+ * inserted into it. [insertBottomUp] is called after all children have been created and
+ * inserted.
+ *
+ * Some trees are faster to build top-down, in which case the [insertTopDown] method should
+ * be used to insert the [instance]. Other tress are faster to build bottom-up in which case
+ * [insertBottomUp] should be used.
+ *
+ * To give example of building a tree top-down vs. bottom-up consider the following tree,
+ *
+ * ```
+ * R
+ * |
+ * B
+ * / \
+ * A C
+ * ```
+ *
+ * where the node `B` is being inserted into the tree at `R`. Top-down building of the tree
+ * first inserts `B` into `R`, then inserts `A` into `B` followed by inserting `C` into B`.
+ * For example,
+ *
+ * ```
+ * 1 2 3
+ * R R R
+ * | | |
+ * B B B
+ * / / \
+ * A A C
+ * ```
+ *
+ * A bottom-up building of the tree starts with inserting `A` and `C` into `B` then inserts
+ * `B` tree into `R`.
+ *
+ * ```
+ * 1 2 3
+ * B B R
+ * | / \ |
+ * A A C B
+ * / \
+ * A C
+ * ```
+ *
+ * To see how building top-down vs. bottom-up can differ significantly in performance
+ * consider a tree where whenever a child is added to the tree all parent nodes, up to the root,
+ * are notified of the new child entering the tree. If the tree is built top-down,
+ *
+ * 1. `R` is notified of `B` entering.
+ * 2. `B` is notified of `A` entering, `R` is notified of `A` entering.
+ * 3. `B` is notified of `C` entering, `R` is notified of `C` entering.
+ *
+ * for a total of 5 notifications. The number of notifications grows exponentially with the
+ * number of inserts.
+ *
+ * For bottom-up, the notifications are,
+ *
+ * 1. `B` is notified `A` entering.
+ * 2. `B` is notified `C` entering.
+ * 3. `R` is notified `B` entering.
+ *
+ * The notifications are linear to the number of nodes inserted.
+ *
+ * If, on the other hand, all children are notified when the parent enters a tree, then the
+ * notifications are, for top-down,
+ *
+ * 1. `B` is notified it is entering `R`.
+ * 2. `A` is notified it is entering `B`.
+ * 3. `C` is notified it is entering `B`.
+ *
+ * which is linear to the number of nodes inserted.
+ *
+ * For bottom-up, the notifications look like,
+ *
+ * 1. `A` is notified it is entering `B`.
+ * 2. `C` is notified it is entering `B`.
+ * 3. `B` is notified it is entering `R`, `A` is notified it is entering `R`,
+ * `C` is notified it is entering `R`.
+ *
+ * which exponential to the number of nodes inserted.
*/
- fun insert(index: Int, instance: N)
+ fun insertTopDown(index: Int, instance: N)
+
+ /**
+ * Indicates that [instance] should be inserted as a child of [current] at [index]. An applier
+ * should insert the node into the tree either in [insertTopDown] or [insertBottomUp], not
+ * both. See the description of [insertTopDown] to which describes when to implement
+ * [insertTopDown] and when to use [insertBottomUp].
+ */
+ fun insertBottomUp(index: Int, instance: N)
/**
* Indicates that the children of [current] from [index] to [index] + [count] should be removed.
@@ -75,10 +163,9 @@
fun remove(index: Int, count: Int)
/**
- * Indicates that the children of [current] from [from] to [from] + [count] should be moved
- * to [to] + [count].
+ * Indicates that [count] children of [current] should be moved from index [from] to index [to].
*
- * The [to] index is related to the position before the change, so, for example, to move an
+ * The [to] index is relative to the position before the change, so, for example, to move an
* element at position 1 to after the element at position 2, [from] should be `1` and [to]
* should be `3`. If the elements were A B C D E, calling `move(1, 3, 1)` would result in the
* elements being reordered to A C B D E.
@@ -93,7 +180,7 @@
}
/**
- * An abstract [Applier] implementation that builds the tree "top down".
+ * An abstract [Applier] implementation.
*
* @sample androidx.compose.runtime.samples.CustomTreeComposition
*
@@ -102,7 +189,6 @@
* @see Composer
* @see emit
*/
-@ExperimentalComposeApi
abstract class AbstractApplier<T>(val root: T) : Applier<T> {
private val stack = mutableListOf<T>()
override var current: T = root
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composable.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composable.kt
index de94b47..f2375f85 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composable.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composable.kt
@@ -50,8 +50,14 @@
// foo: (@Composable () -> Unit) -> Unit
AnnotationTarget.TYPE_PARAMETER,
- // composable property declarations
+ // (DEPRECATED) composable property declarations
// @Composable val foo: Int get() { ... }
- AnnotationTarget.PROPERTY
+ AnnotationTarget.PROPERTY,
+
+ // composable property getters and setters
+ // val foo: Int @Composable get() { ... }
+ // var bar: Int
+ // @Composable get() { ... }
+ AnnotationTarget.PROPERTY_GETTER
)
annotation class Composable
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposableContract.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposableContract.kt
index 4a820a8..213e507 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposableContract.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposableContract.kt
@@ -45,7 +45,8 @@
@Retention(AnnotationRetention.BINARY)
@Target(
AnnotationTarget.FUNCTION,
- AnnotationTarget.PROPERTY,
+ AnnotationTarget.PROPERTY, // (DEPRECATED)
+ AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.TYPE
)
annotation class ComposableContract(
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 206e8f4..e39d965 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
@@ -77,7 +77,9 @@
val result = hashMapOf<Int, GroupInfo>()
for (index in 0 until keyInfos.size) {
val keyInfo = keyInfos[index]
+ @OptIn(InternalComposeApi::class)
result[keyInfo.location] = GroupInfo(index, runningNodeIndex, keyInfo.nodes)
+ @OptIn(InternalComposeApi::class)
runningNodeIndex += keyInfo.nodes
}
result
@@ -146,6 +148,7 @@
}
}
+ @OptIn(InternalComposeApi::class)
fun registerInsert(keyInfo: KeyInfo, insertIndex: Int) {
groupInfos[keyInfo.location] = GroupInfo(-1, insertIndex, 0)
}
@@ -167,8 +170,13 @@
return false
}
+ @OptIn(InternalComposeApi::class)
fun slotPositionOf(keyInfo: KeyInfo) = groupInfos[keyInfo.location]?.slotIndex ?: -1
+
+ @OptIn(InternalComposeApi::class)
fun nodePositionOf(keyInfo: KeyInfo) = groupInfos[keyInfo.location]?.nodeIndex ?: -1
+
+ @OptIn(InternalComposeApi::class)
fun updatedNodeCountOf(keyInfo: KeyInfo) =
groupInfos[keyInfo.location]?.nodeCount ?: keyInfo.nodes
}
@@ -371,7 +379,7 @@
private var nodeCountOverrides: IntArray? = null
private var nodeCountVirtualOverrides: HashMap<Int, Int>? = null
private var collectKeySources = false
-
+ private var collectParameterInformation = false
private var nodeExpected = false
private val observations: MutableList<Any> = mutableListOf()
private val observationsProcessed: MutableList<Any> = mutableListOf()
@@ -535,6 +543,7 @@
* Start the composition. This should be called, and only be called, as the first group in
* the composition.
*/
+ @OptIn(InternalComposeApi::class)
private fun startRoot() {
reader = slotTable.openReader()
startGroup(rootKey)
@@ -545,6 +554,7 @@
providersInvalidStack.push(providersInvalid.asInt())
providersInvalid = changed(parentProvider)
collectKeySources = parentReference.collectingKeySources
+ collectParameterInformation = parentReference.collectingParameterInformation
resolveAmbient(InspectionTables, parentProvider)?.let {
it.add(slotTable)
parentReference.recordInspectionTable(it)
@@ -556,6 +566,7 @@
* End the composition. This should be called, and only be called, to end the first group in
* the composition.
*/
+ @OptIn(InternalComposeApi::class)
private fun endRoot() {
endGroup()
parentReference.doneComposing()
@@ -569,6 +580,7 @@
/**
* Discard a pending composition because an error was encountered during composition
*/
+ @OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
private fun abortRoot() {
cleanUpCompose()
pendingStack.clear()
@@ -621,6 +633,15 @@
}
/**
+ * Start collecting parameter information. This enables the tools API to always be able to
+ * determine the parameter values of composable calls.
+ */
+ @InternalComposeApi
+ fun collectParameterInformation() {
+ collectParameterInformation = true
+ }
+
+ /**
* Record that [value] was read from. If [recordWriteOf] or [recordModificationsOf] is called
* with [value] then the corresponding [currentRecomposeScope] is invalidated.
*
@@ -756,6 +777,7 @@
* Apply the changes to the tree that were collected during the last composition.
*/
@InternalComposeApi
+ @OptIn(ExperimentalComposeApi::class)
fun applyChanges() {
trace("Compose:applyChanges") {
invalidateStack.clear()
@@ -804,6 +826,7 @@
}
@ExperimentalComposeApi
+ @OptIn(InternalComposeApi::class)
internal fun dispose() {
trace("Compose:Composer.dispose") {
parentReference.unregisterComposer(this)
@@ -845,6 +868,7 @@
*/
internal fun endGroup() = end(false)
+ @OptIn(InternalComposeApi::class)
private fun skipGroup() {
groupNodeCount += reader.skipGroup()
}
@@ -867,6 +891,7 @@
* call when the composer is inserting.
*/
@Suppress("UNUSED")
+ @OptIn(ExperimentalComposeApi::class)
internal fun <T : N> createNode(factory: () -> T) {
validateNodeExpected()
check(inserting) { "createNode() can only be called when inserting" }
@@ -876,9 +901,14 @@
recordFixup { applier, slots, _ ->
val node = factory()
slots.node = node
- applier.insert(insertIndex, node)
+ applier.insertTopDown(insertIndex, node)
applier.down(node)
}
+ recordInsertUp { applier, _, _ ->
+ val nodeToInsert = applier.current
+ applier.up()
+ applier.insertBottomUp(insertIndex, nodeToInsert)
+ }
}
/**
@@ -886,6 +916,7 @@
* inserting.
*/
@PublishedApi
+ @OptIn(ExperimentalComposeApi::class)
internal fun emitNode(node: Any?) {
validateNodeExpected()
check(inserting) { "emitNode() called when not inserting" }
@@ -895,9 +926,14 @@
@Suppress("UNCHECKED_CAST")
writer.node = node as N
recordApplierOperation { applier, _, _ ->
- applier.insert(insertIndex, node)
+ applier.insertTopDown(insertIndex, node)
applier.down(node)
}
+ recordInsertUp { applier, _, _ ->
+ val nodeToInsert = applier.current
+ applier.up()
+ applier.insertBottomUp(insertIndex, nodeToInsert)
+ }
}
/**
@@ -906,6 +942,7 @@
* location as [emitNode] or [createNode] as called even if the value is unused.
*/
@PublishedApi
+ @OptIn(InternalComposeApi::class)
internal fun useNode(): N {
validateNodeExpected()
check(!inserting) { "useNode() called while inserting" }
@@ -925,6 +962,7 @@
* node that is the current node in the tree which was either created by [createNode],
* emitted by [emitNode] or returned by [useNode].
*/
+ @OptIn(ExperimentalComposeApi::class)
internal fun <V, T> apply(value: V, block: T.(V) -> Unit) {
recordApplierOperation { applier, _, _ ->
@Suppress("UNCHECKED_CAST")
@@ -937,6 +975,7 @@
* use the key stored at the current location in the slot table to avoid allocating a new key.
*/
@ComposeCompilerApi
+ @OptIn(InternalComposeApi::class)
fun joinKey(left: Any?, right: Any?): Any =
getKey(reader.groupObjectKey, left, right) ?: JoinedKey(left, right)
@@ -944,6 +983,7 @@
* Return the next value in the slot table and advance the current location.
*/
@PublishedApi
+ @OptIn(InternalComposeApi::class)
internal fun nextSlot(): Any? = if (inserting) {
validateNodeNotExpected()
EMPTY
@@ -1081,6 +1121,7 @@
* @param value the value to schedule to be written to the slot table.
*/
@PublishedApi
+ @OptIn(InternalComposeApi::class)
internal fun updateValue(value: Any?) {
if (inserting) {
writer.update(value)
@@ -1256,11 +1297,16 @@
startGroup(referenceKey, reference)
var ref = nextSlot() as? CompositionReferenceHolder<*>
- if (ref == null || !inserting) {
+ if (ref == null) {
val scope = invalidateStack.peek()
scope.used = true
ref = CompositionReferenceHolder(
- CompositionReferenceImpl(scope, currentCompoundKeyHash, collectKeySources)
+ CompositionReferenceImpl(
+ scope,
+ currentCompoundKeyHash,
+ collectKeySources,
+ collectParameterInformation
+ )
)
updateValue(ref)
}
@@ -1567,7 +1613,7 @@
val inserting = inserting
if (inserting) {
if (isNode) {
- recordInsertUp()
+ registerInsertUp()
expectedNodeCount = 1
}
reader.endEmpty()
@@ -1588,8 +1634,8 @@
if (isNode) recordUp()
recordEndGroup()
val parentGroup = reader.parent
- val parentNodecount = updatedNodeCount(parentGroup)
- if (expectedNodeCount != parentNodecount) {
+ val parentNodeCount = updatedNodeCount(parentGroup)
+ if (expectedNodeCount != parentNodeCount) {
updateNodeCountOverrides(parentGroup, expectedNodeCount)
}
if (isNode) {
@@ -1967,7 +2013,7 @@
val scope = if (invalidateStack.isNotEmpty()) invalidateStack.pop()
else null
scope?.requiresRecompose = false
- val result = if (scope != null && (scope.used || collectKeySources)) {
+ val result = if (scope != null && (scope.used || collectParameterInformation)) {
if (scope.anchor == null) {
scope.anchor = if (inserting) {
writer.anchor(writer.parent)
@@ -2038,11 +2084,14 @@
internal fun hasInvalidations() = invalidations.isNotEmpty()
@Suppress("UNCHECKED_CAST")
+ @OptIn(InternalComposeApi::class)
private var SlotWriter.node
get() = node(currentGroup) as N
set(value) { updateParentNode(value) }
+
@Suppress("UNCHECKED_CAST")
private val SlotReader.node get() = node(parent) as N
+
@Suppress("UNCHECKED_CAST")
private fun SlotReader.nodeAt(index: Int) = node(index) as N
@@ -2145,20 +2194,32 @@
}
}
- private var pendingInsertUps = 0
+ private var insertUpRequests = Stack<Change<N>>()
- private fun recordInsertUp() {
- pendingInsertUps++
+ private var pendingInsertUps = mutableListOf<Change<N>>()
+
+ private fun registerInsertUp() {
+ pendingInsertUps.add(insertUpRequests.pop())
}
private fun realizeInsertUps() {
- if (pendingInsertUps > 0) {
- val count = pendingInsertUps
- record { applier, _, _ -> repeat(count) { applier.up() } }
- pendingInsertUps = 0
+ if (pendingInsertUps.isNotEmpty()) {
+ pendingInsertUps.forEach { record(it) }
+ pendingInsertUps.clear()
}
}
+ /**
+ * Record a change that will be added after all the changes for the node and all its children
+ * have been performed.
+ *
+ * This is used to implement calling [Applier.insertBottomUp] to allow a tree to be built
+ * bottom-up instead of top-down.
+ */
+ private fun recordInsertUp(change: Change<N>) {
+ insertUpRequests.push(change)
+ }
+
// Navigating the writer slot is performed relatively as the location of a group in the writer
// might be different than it is in the reader as groups can be inserted, deleted, or moved.
//
@@ -2384,7 +2445,8 @@
private inner class CompositionReferenceImpl(
val scope: RecomposeScope,
override val compoundHashKey: Int,
- override val collectingKeySources: Boolean
+ override val collectingKeySources: Boolean,
+ override val collectingParameterInformation: Boolean
) : CompositionReference() {
var inspectionTables: MutableSet<MutableSet<SlotTable>>? = null
val composers = mutableSetOf<Composer<*>>()
@@ -2428,7 +2490,17 @@
}
override fun invalidate(composer: Composer<*>) {
- invalidate(scope)
+ // Invalidate ourselves with our parent before we invalidate a child composer.
+ // This ensures that when we are scheduling recompositions, parents always
+ // recompose before their children just in case a recomposition in the parent
+ // would also cause other recomposition in the child.
+ // If the parent ends up having no real invalidations to process we will skip work
+ // for that composer along a fast path later.
+ // This invalidation process could be made more efficient as it's currently N^2 with
+ // subcomposition meta-tree depth thanks to the double recursive parent walk
+ // performed here, but we currently assume a low N.
+ parentReference.invalidate(this@Composer)
+ parentReference.invalidate(composer)
}
override fun <T> getAmbient(key: Ambient<T>): T {
@@ -2618,7 +2690,7 @@
// Observation helpers
-// These helpers enable storing observaction pairs of value to scope instances in a list sorted by
+// These helpers enable storing observation pairs of value to scope instances in a list sorted by
// the value hash as a primary key and the scope hash as the secondary key. This results in a
// multi-set that allows finding an observation/scope pairs in O(log N) and a worst case of
// insert into and remove from the array of O(log N) + O(N) where N is the number of total pairs.
@@ -2798,8 +2870,7 @@
private fun Boolean.asInt() = if (this) 1 else 0
private fun Int.asBool() = this != 0
-@Composable
-val currentComposer: Composer<*> get() {
+val currentComposer: Composer<*> @Composable get() {
throw NotImplementedError("Implemented as an intrinsic")
}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionReference.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionReference.kt
index 51f7320..26940dd 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionReference.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionReference.kt
@@ -36,6 +36,7 @@
abstract class CompositionReference internal constructor() {
internal abstract val compoundHashKey: Int
internal abstract val collectingKeySources: Boolean
+ internal abstract val collectingParameterInformation: Boolean
internal abstract val effectCoroutineContext: CoroutineContext
internal abstract fun composeInitial(composer: Composer<*>, composable: @Composable () -> Unit)
internal abstract fun invalidate(composer: Composer<*>)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Effects.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Effects.kt
index ea257ff..5f2a2b37 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Effects.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Effects.kt
@@ -197,8 +197,7 @@
* An Effect to get the nearest invalidation lambda to the current point of composition. This can be used to
* trigger an invalidation on the composition locally to cause a recompose.
*/
-@Composable
-val invalidate: () -> Unit get() {
+val invalidate: () -> Unit @Composable get() {
val scope = currentComposer.currentRecomposeScope ?: error("no recompose scope found")
scope.used = true
return { scope.invalidate() }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index b543735..03bdc3d 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
@@ -433,6 +433,10 @@
internal override val collectingKeySources: Boolean
get() = false
+ // Collecting parameter happens at the level of a composer; starts as false
+ internal override val collectingParameterInformation: Boolean
+ get() = false
+
internal override fun recordInspectionTable(table: MutableSet<SlotTable>) {
// TODO: The root recomposer might be a better place to set up inspection
// than the current configuration with an ambient
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
index f5e27ed..b1f49c7 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
@@ -773,7 +773,7 @@
fun reposition(index: Int) {
require(emptyCount == 0) { "Cannot reposition while in an empty region" }
currentGroup = index
- val parent = groups.parentAnchor(index)
+ val parent = if (index < groupsSize) groups.parentAnchor(index) else -1
this.parent = parent
if (parent < 0)
this.currentEnd = groupsSize
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/LiveLiteral.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/LiveLiteral.kt
index eecd738..b00fb76 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/LiveLiteral.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/LiveLiteral.kt
@@ -51,7 +51,13 @@
private val liveLiteralCache = HashMap<String, MutableState<Any?>>()
@InternalComposeApi
-val isLiveLiteralsEnabled: Boolean = false
+var isLiveLiteralsEnabled: Boolean = false
+ private set
+
+@InternalComposeApi
+fun enableLiveLiterals() {
+ isLiveLiteralsEnabled = true
+}
@InternalComposeApi
fun <T> liveLiteral(key: String, value: T): State<T> {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt
index aa29075..132b707 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt
@@ -88,9 +88,11 @@
override fun clear() = writable { list = persistentListOf() }
override fun remove(element: T) = conditionalUpdate { it.remove(element) }
override fun removeAll(elements: Collection<T>) = conditionalUpdate { it.removeAll(elements) }
- override fun removeAt(index: Int) = get(index).also { update { it.removeAt(index) } }
+ override fun removeAt(index: Int): T = get(index).also { update { it.removeAt(index) } }
override fun retainAll(elements: Collection<T>) = mutate { it.retainAll(elements) }
- override fun set(index: Int, element: T) = get(index).also { update { it.set(index, element) } }
+ override fun set(index: Int, element: T): T = get(index).also {
+ update { it.set(index, element) }
+ }
fun removeRange(fromIndex: Int, toIndex: Int) {
mutate {
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/AbstractApplierTest.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/AbstractApplierTest.kt
index e80dbdb..59fcfd2 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/AbstractApplierTest.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/AbstractApplierTest.kt
@@ -35,22 +35,22 @@
@Test fun downGoesDown() {
val one = Node("one")
- applier.insert(0, one)
+ applier.insertTopDown(0, one)
applier.down(one)
assertSame(one, applier.current)
val two = Node("two")
- applier.insert(0, two)
+ applier.insertTopDown(0, two)
applier.down(two)
assertSame(two, applier.current)
}
@Test fun upGoesUp() {
val one = Node("one")
- applier.insert(0, one)
+ applier.insertTopDown(0, one)
applier.down(one)
val two = Node("two")
- applier.insert(0, two)
+ applier.insertTopDown(0, two)
applier.down(two)
applier.up()
@@ -61,7 +61,7 @@
@Test fun clearClearsAndPointsToRoot() {
val child = Node("child")
- applier.insert(0, child)
+ applier.insertTopDown(0, child)
applier.down(child)
applier.clear()
@@ -76,10 +76,10 @@
val two = Node("two")
val three = Node("three")
val four = Node("four")
- applier.insert(0, one)
- applier.insert(1, two)
- applier.insert(2, three)
- applier.insert(3, four)
+ applier.insertTopDown(0, one)
+ applier.insertTopDown(1, two)
+ applier.insertTopDown(2, three)
+ applier.insertTopDown(3, four)
applier.remove(1, 1) // Middle
assertEquals(listOf(one, three, four), root.children)
@@ -99,13 +99,13 @@
val five = Node("five")
val six = Node("six")
val seven = Node("seven")
- applier.insert(0, one)
- applier.insert(1, two)
- applier.insert(2, three)
- applier.insert(3, four)
- applier.insert(4, five)
- applier.insert(5, six)
- applier.insert(6, seven)
+ applier.insertTopDown(0, one)
+ applier.insertTopDown(1, two)
+ applier.insertTopDown(2, three)
+ applier.insertTopDown(3, four)
+ applier.insertTopDown(4, five)
+ applier.insertTopDown(5, six)
+ applier.insertTopDown(6, seven)
applier.remove(2, 2) // Middle
assertEquals(listOf(one, two, five, six, seven), root.children)
@@ -121,9 +121,9 @@
val one = Node("one")
val two = Node("two")
val three = Node("three")
- applier.insert(0, one)
- applier.insert(1, two)
- applier.insert(2, three)
+ applier.insertTopDown(0, one)
+ applier.insertTopDown(1, two)
+ applier.insertTopDown(2, three)
applier.move(0, 3, 1)
assertEquals(listOf(two, three, one), root.children)
@@ -141,9 +141,9 @@
val one = Node("one")
val two = Node("two")
val three = Node("three")
- applier.insert(0, one)
- applier.insert(1, two)
- applier.insert(2, three)
+ applier.insertTopDown(0, one)
+ applier.insertTopDown(1, two)
+ applier.insertTopDown(2, three)
applier.move(2, 0, 1)
assertEquals(listOf(three, one, two), root.children)
@@ -162,10 +162,10 @@
val two = Node("two")
val three = Node("three")
val four = Node("four")
- applier.insert(0, one)
- applier.insert(1, two)
- applier.insert(2, three)
- applier.insert(3, four)
+ applier.insertTopDown(0, one)
+ applier.insertTopDown(1, two)
+ applier.insertTopDown(2, three)
+ applier.insertTopDown(3, four)
applier.move(0, 4, 2)
assertEquals(listOf(three, four, one, two), root.children)
@@ -178,10 +178,10 @@
val two = Node("two")
val three = Node("three")
val four = Node("four")
- applier.insert(0, one)
- applier.insert(1, two)
- applier.insert(2, three)
- applier.insert(3, four)
+ applier.insertTopDown(0, one)
+ applier.insertTopDown(1, two)
+ applier.insertTopDown(2, three)
+ applier.insertTopDown(3, four)
applier.move(2, 0, 2)
assertEquals(listOf(three, four, one, two), root.children)
@@ -195,10 +195,12 @@
@OptIn(ExperimentalComposeApi::class)
private class NodeApplier(root: Node) : AbstractApplier<Node>(root) {
- override fun insert(index: Int, instance: Node) {
+ override fun insertTopDown(index: Int, instance: Node) {
current.children.add(index, instance)
}
+ override fun insertBottomUp(index: Int, instance: Node) { }
+
override fun remove(index: Int, count: Int) {
current.children.remove(index, count)
}
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
index 1374742b..fde53a01 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -25,6 +25,7 @@
import androidx.compose.runtime.mock.MockViewValidator
import androidx.compose.runtime.mock.Point
import androidx.compose.runtime.mock.Report
+import androidx.compose.runtime.mock.TestMonotonicFrameClock
import androidx.compose.runtime.mock.View
import androidx.compose.runtime.mock.ViewApplier
import androidx.compose.runtime.mock.contact
@@ -41,9 +42,12 @@
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.snapshots.takeMutableSnapshot
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.test.runBlockingTest
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -2503,6 +2507,129 @@
validate()
}
}
+
+ /**
+ * This test checks that an updated ComposableLambda capture used in a subcomposition
+ * correctly invalidates that subcomposition and schedules recomposition of that subcomposition.
+ */
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testComposableLambdaSubcompositionInvalidation() = runBlockingTest {
+ localRecomposerTest { recomposer ->
+ val composer = Composer(SlotTable(), EmptyApplier(), recomposer)
+ try {
+ var rootState by mutableStateOf(false)
+ val composedResults = mutableListOf<Boolean>()
+ Snapshot.notifyObjectsInitialized()
+ recomposer.composeInitial(composer) {
+ // Read into local variable, local will be captured below
+ val capturedValue = rootState
+ TestSubcomposition {
+ composedResults.add(capturedValue)
+ }
+ }
+ composer.applyChanges()
+ assertEquals(listOf(false), composedResults)
+ rootState = true
+ Snapshot.sendApplyNotifications()
+ advanceUntilIdle()
+ assertEquals(listOf(false, true), composedResults)
+ } finally {
+ composer.dispose()
+ }
+ }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testCompositionReferenceIsRemembered() = runBlockingTest {
+ localRecomposerTest { recomposer ->
+ val composer = Composer(SlotTable(), EmptyApplier(), recomposer)
+ try {
+ lateinit var invalidator: () -> Unit
+ val parentReferences = mutableListOf<CompositionReference>()
+ recomposer.composeInitial(composer) {
+ invalidator = invalidate
+ parentReferences += compositionReference()
+ }
+ composer.applyChanges()
+ invalidator()
+ advanceUntilIdle()
+ assert(parentReferences.size > 1) { "expected to be composed more than once" }
+ assert(parentReferences.toSet().size == 1) {
+ "expected all parentReferences to be the same; saw $parentReferences"
+ }
+ } finally {
+ composer.dispose()
+ }
+ }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testParentCompositionRecomposesFirst() = runBlockingTest {
+ localRecomposerTest { recomposer ->
+ val composer = Composer(SlotTable(), EmptyApplier(), recomposer)
+ val results = mutableListOf<String>()
+ try {
+ var firstState by mutableStateOf("firstInitial")
+ var secondState by mutableStateOf("secondInitial")
+ Snapshot.notifyObjectsInitialized()
+ recomposer.composeInitial(composer) {
+ results += firstState
+ TestSubcomposition {
+ results += secondState
+ }
+ }
+ secondState = "secondSet"
+ Snapshot.sendApplyNotifications()
+ firstState = "firstSet"
+ Snapshot.sendApplyNotifications()
+ advanceUntilIdle()
+ assertEquals(
+ listOf("firstInitial", "secondInitial", "firstSet", "secondSet"),
+ results,
+ "Expected call ordering during recomposition of subcompositions"
+ )
+ } finally {
+ composer.dispose()
+ }
+ }
+ }
+}
+
+@OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
+@Composable
+private fun TestSubcomposition(
+ content: @Composable () -> Unit
+) {
+ val parentRef = compositionReference()
+ val currentContent by rememberUpdatedState(content)
+ DisposableEffect(parentRef) {
+ val subcomposer = Composer(SlotTable(), EmptyApplier(), parentRef)
+ parentRef.composeInitial(subcomposer) {
+ currentContent()
+ }
+ subcomposer.applyChanges()
+ onDispose {
+ subcomposer.dispose()
+ }
+ }
+}
+
+@OptIn(ExperimentalCoroutinesApi::class)
+private suspend fun <R> localRecomposerTest(
+ block: CoroutineScope.(Recomposer) -> R
+) = coroutineScope {
+ val contextWithClock = coroutineContext + TestMonotonicFrameClock(this)
+ val recomposer = Recomposer(contextWithClock)
+ launch(contextWithClock) {
+ recomposer.runRecomposeAndApplyChanges()
+ }
+ block(recomposer)
+ // This call doesn't need to be in a finally; everything it does will be torn down
+ // in exceptional cases by the coroutineScope failure
+ recomposer.shutDown()
}
@Composable fun Wrap(content: @Composable () -> Unit) {
@@ -2664,3 +2791,23 @@
private interface Named {
val name: String
}
+
+@OptIn(ExperimentalComposeApi::class)
+private class EmptyApplier : Applier<Unit> {
+ override val current: Unit = Unit
+ override fun down(node: Unit) {}
+ override fun up() {}
+ override fun insertTopDown(index: Int, instance: Unit) {
+ error("Unexpected")
+ }
+ override fun insertBottomUp(index: Int, instance: Unit) {
+ error("Unexpected")
+ }
+ override fun remove(index: Int, count: Int) {
+ error("Unexpected")
+ }
+ override fun move(from: Int, to: Int, count: Int) {
+ error("Unexpected")
+ }
+ override fun clear() {}
+}
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/SlotTableTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/SlotTableTests.kt
index 2f61e12..5910f88 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/SlotTableTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/SlotTableTests.kt
@@ -3047,6 +3047,26 @@
}
}
}
+
+ @Test
+ fun canRepositionReaderPastEndOfTable() {
+ val slots = SlotTable().also {
+ it.write { writer ->
+ // Create exactly 256 groups
+ repeat(256) {
+ writer.insert {
+ writer.startGroup(0)
+ writer.endGroup()
+ }
+ }
+ }
+ }
+
+ slots.read { reader ->
+ reader.reposition(reader.size)
+ // Expect the above not to crash.
+ }
+ }
}
@OptIn(InternalComposeApi::class)
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/mock/TestMonotonicFrameClock.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/mock/TestMonotonicFrameClock.kt
new file mode 100644
index 0000000..d7d6eb2
--- /dev/null
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/mock/TestMonotonicFrameClock.kt
@@ -0,0 +1,122 @@
+/*
+ * 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.
+ */
+
+/*
+ * NOTE: This file is copied from androidx.compose.ui.test for use in testing compose-runtime.
+ * A future patch may graduate this to a formal compose-runtime-test module.
+ */
+package androidx.compose.runtime.mock
+
+import androidx.compose.runtime.dispatch.MonotonicFrameClock
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.test.DelayController
+import kotlin.coroutines.ContinuationInterceptor
+
+private const val DefaultFrameDelay = 16_000_000L
+
+/**
+ * Construct a [TestMonotonicFrameClock] for [coroutineScope], obtaining the [DelayController]
+ * from the scope's [context][CoroutineScope.coroutineContext]. This frame clock may be used to
+ * consistently drive time under controlled tests.
+ *
+ * Calls to [TestMonotonicFrameClock.withFrameNanos] will schedule an upcoming frame
+ * [frameDelayNanos] nanoseconds in the future by launching into [coroutineScope] if such a frame
+ * has not yet been scheduled.
+ */
+@Suppress("MethodNameUnits") // Nanos for high-precision animation clocks
+@ExperimentalCoroutinesApi
+fun TestMonotonicFrameClock(
+ coroutineScope: CoroutineScope,
+ frameDelayNanos: Long = DefaultFrameDelay
+): TestMonotonicFrameClock = TestMonotonicFrameClock(
+ coroutineScope = coroutineScope,
+ delayController = coroutineScope.coroutineContext[ContinuationInterceptor].let { interceptor ->
+ requireNotNull(interceptor as? DelayController) {
+ "ContinuationInterceptor $interceptor of supplied scope must implement DelayController"
+ }
+ },
+ frameDelayNanos = frameDelayNanos
+)
+
+/**
+ * A [MonotonicFrameClock] with a time source controlled by a `kotlinx-coroutines-test`
+ * [DelayController]. This frame clock may be used to consistently drive time under controlled
+ * tests.
+ *
+ * Calls to [withFrameNanos] will schedule an upcoming frame [frameDelayNanos] nanoseconds in the
+ * future by launching into [coroutineScope] if such a frame has not yet been scheduled. The
+ * current frame time for [withFrameNanos] is provided by [delayController]. It is strongly
+ * suggested that [coroutineScope] contain the test dispatcher controlled by [delayController].
+ */
+@ExperimentalCoroutinesApi
+class TestMonotonicFrameClock(
+ private val coroutineScope: CoroutineScope,
+ private val delayController: DelayController,
+ @get:Suppress("MethodNameUnits") // Nanos for high-precision animation clocks
+ val frameDelayNanos: Long = DefaultFrameDelay
+) : MonotonicFrameClock {
+ private val lock = Any()
+ private val awaiters = mutableListOf<Awaiter<*>>()
+ private var posted = false
+
+ private class Awaiter<R>(
+ private val onFrame: (Long) -> R,
+ private val continuation: CancellableContinuation<R>
+ ) {
+ fun runFrame(frameTimeNanos: Long): () -> Unit {
+ val result = runCatching { onFrame(frameTimeNanos) }
+ return { continuation.resumeWith(result) }
+ }
+ }
+
+ override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R =
+ suspendCancellableCoroutine { co ->
+ synchronized(lock) {
+ awaiters.add(Awaiter(onFrame, co))
+ maybeLaunchTickRunner()
+ }
+ }
+
+ private fun maybeLaunchTickRunner() {
+ if (!posted) {
+ posted = true
+ coroutineScope.launch {
+ delay(frameDelayMillis)
+ synchronized(lock) {
+ posted = false
+ val toRun = awaiters.toList()
+ awaiters.clear()
+ val frameTime = delayController.currentTime * 1_000_000
+ // In case of awaiters on an immediate dispatcher, run all frame callbacks
+ // before resuming any associated continuations with the results.
+ toRun.map { it.runFrame(frameTime) }.forEach { it() }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * The frame delay time for the [TestMonotonicFrameClock] in milliseconds.
+ */
+@ExperimentalCoroutinesApi
+val TestMonotonicFrameClock.frameDelayMillis: Long
+ get() = frameDelayNanos / 1_000_000
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/mock/ViewApplier.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/mock/ViewApplier.kt
index 535c550..ae08758 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/mock/ViewApplier.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/mock/ViewApplier.kt
@@ -33,7 +33,11 @@
var onEndChangesCalled = 0
private set
- override fun insert(index: Int, instance: View) {
+ override fun insertTopDown(index: Int, instance: View) {
+ // Ignored as the tree is built bottom-up.
+ }
+
+ override fun insertBottomUp(index: Int, instance: View) {
current.addAt(index, instance)
}
diff --git a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.kt b/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.kt
index cdc65bb..87326b1 100644
--- a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.kt
+++ b/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.kt
@@ -34,7 +34,7 @@
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.dispatch.MonotonicFrameClock
import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.platform.ViewRootForTest
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.test.TestMonotonicFrameClock
import androidx.compose.ui.test.frameDelayMillis
@@ -134,7 +134,7 @@
}
composition = activity.setContent(recomposer) { testCase!!.Content() }
- view = findAndroidOwner(activity)!!.view
+ view = findViewRootForTest(activity)!!.view
@OptIn(ExperimentalComposeApi::class)
Snapshot.notifyObjectsInitialized()
simulationState = SimulationState.EmitContentDone
@@ -308,18 +308,18 @@
RecomposeDone
}
-private fun findAndroidOwner(activity: Activity): AndroidOwner? {
- return findAndroidOwner(activity.findViewById(android.R.id.content) as ViewGroup)
+private fun findViewRootForTest(activity: Activity): ViewRootForTest? {
+ return findViewRootForTest(activity.findViewById(android.R.id.content) as ViewGroup)
}
-private fun findAndroidOwner(view: View): AndroidOwner? {
- if (view is AndroidOwner) {
+private fun findViewRootForTest(view: View): ViewRootForTest? {
+ if (view is ViewRootForTest) {
return view
}
if (view is ViewGroup) {
for (i in 0 until view.childCount) {
- val composeView = findAndroidOwner(view.getChildAt(i))
+ val composeView = findViewRootForTest(view.getChildAt(i))
if (composeView != null) {
return composeView
}
diff --git a/compose/ui/ui-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index 2b56325..b18ab3f 100644
--- a/compose/ui/ui-graphics/api/current.txt
+++ b/compose/ui/ui-graphics/api/current.txt
@@ -31,6 +31,7 @@
method public androidx.compose.ui.graphics.ColorFilter? getColorFilter();
method public androidx.compose.ui.graphics.FilterQuality getFilterQuality();
method public android.graphics.PathEffect? getNativePathEffect();
+ method public androidx.compose.ui.graphics.PathEffect? getPathEffect();
method public android.graphics.Shader? getShader();
method public androidx.compose.ui.graphics.StrokeCap getStrokeCap();
method public androidx.compose.ui.graphics.StrokeJoin getStrokeJoin();
@@ -45,6 +46,7 @@
method public void setColorFilter(androidx.compose.ui.graphics.ColorFilter? value);
method public void setFilterQuality(androidx.compose.ui.graphics.FilterQuality value);
method public void setNativePathEffect(android.graphics.PathEffect? value);
+ method public void setPathEffect(androidx.compose.ui.graphics.PathEffect? value);
method public void setShader(android.graphics.Shader? value);
method public void setStrokeCap(androidx.compose.ui.graphics.StrokeCap value);
method public void setStrokeJoin(androidx.compose.ui.graphics.StrokeJoin value);
@@ -58,6 +60,7 @@
property public androidx.compose.ui.graphics.FilterQuality filterQuality;
property public boolean isAntiAlias;
property public android.graphics.PathEffect? nativePathEffect;
+ property public androidx.compose.ui.graphics.PathEffect? pathEffect;
property public android.graphics.Shader? shader;
property public androidx.compose.ui.graphics.StrokeCap strokeCap;
property public androidx.compose.ui.graphics.StrokeJoin strokeJoin;
@@ -104,6 +107,11 @@
property public boolean isEmpty;
}
+ public final class AndroidPathEffectKt {
+ method public static android.graphics.PathEffect asAndroidPathEffect(androidx.compose.ui.graphics.PathEffect);
+ method public static androidx.compose.ui.graphics.PathEffect toComposePathEffect(android.graphics.PathEffect);
+ }
+
public final class AndroidPathKt {
method public static androidx.compose.ui.graphics.Path Path();
method public static inline android.graphics.Path asAndroidPath(androidx.compose.ui.graphics.Path);
@@ -430,7 +438,8 @@
method public long getColor-0d7_KjU();
method public androidx.compose.ui.graphics.ColorFilter? getColorFilter();
method public androidx.compose.ui.graphics.FilterQuality getFilterQuality();
- method public android.graphics.PathEffect? getNativePathEffect();
+ method @Deprecated public android.graphics.PathEffect? getNativePathEffect();
+ method public androidx.compose.ui.graphics.PathEffect? getPathEffect();
method public android.graphics.Shader? getShader();
method public androidx.compose.ui.graphics.StrokeCap getStrokeCap();
method public androidx.compose.ui.graphics.StrokeJoin getStrokeJoin();
@@ -444,7 +453,8 @@
method public void setColor-8_81llA(long p);
method public void setColorFilter(androidx.compose.ui.graphics.ColorFilter? p);
method public void setFilterQuality(androidx.compose.ui.graphics.FilterQuality p);
- method public void setNativePathEffect(android.graphics.PathEffect? p);
+ method @Deprecated public void setNativePathEffect(android.graphics.PathEffect? p);
+ method public void setPathEffect(androidx.compose.ui.graphics.PathEffect? p);
method public void setShader(android.graphics.Shader? p);
method public void setStrokeCap(androidx.compose.ui.graphics.StrokeCap p);
method public void setStrokeJoin(androidx.compose.ui.graphics.StrokeJoin p);
@@ -457,7 +467,8 @@
property public abstract androidx.compose.ui.graphics.ColorFilter? colorFilter;
property public abstract androidx.compose.ui.graphics.FilterQuality filterQuality;
property public abstract boolean isAntiAlias;
- property public abstract android.graphics.PathEffect? nativePathEffect;
+ property @Deprecated public abstract android.graphics.PathEffect? nativePathEffect;
+ property public abstract androidx.compose.ui.graphics.PathEffect? pathEffect;
property public abstract android.graphics.Shader? shader;
property public abstract androidx.compose.ui.graphics.StrokeCap strokeCap;
property public abstract androidx.compose.ui.graphics.StrokeJoin strokeJoin;
@@ -511,6 +522,17 @@
method public androidx.compose.ui.graphics.Path combine(androidx.compose.ui.graphics.PathOperation operation, androidx.compose.ui.graphics.Path path1, androidx.compose.ui.graphics.Path path2);
}
+ public interface PathEffect {
+ field public static final androidx.compose.ui.graphics.PathEffect.Companion Companion;
+ }
+
+ public static final class PathEffect.Companion {
+ method public androidx.compose.ui.graphics.PathEffect chainPathEffect(androidx.compose.ui.graphics.PathEffect outer, androidx.compose.ui.graphics.PathEffect inner);
+ method public androidx.compose.ui.graphics.PathEffect cornerPathEffect(float radius);
+ method public androidx.compose.ui.graphics.PathEffect dashPathEffect(float[] intervals, optional float phase);
+ method public androidx.compose.ui.graphics.PathEffect stampedPathEffect(androidx.compose.ui.graphics.Path shape, float advance, float phase, androidx.compose.ui.graphics.StampedPathEffectStyle style);
+ }
+
public enum PathFillType {
enum_constant public static final androidx.compose.ui.graphics.PathFillType EvenOdd;
enum_constant public static final androidx.compose.ui.graphics.PathFillType NonZero;
@@ -612,6 +634,12 @@
property public final long value;
}
+ public enum StampedPathEffectStyle {
+ enum_constant public static final androidx.compose.ui.graphics.StampedPathEffectStyle Morph;
+ enum_constant public static final androidx.compose.ui.graphics.StampedPathEffectStyle Rotate;
+ enum_constant public static final androidx.compose.ui.graphics.StampedPathEffectStyle Translate;
+ }
+
public enum StrokeCap {
enum_constant public static final androidx.compose.ui.graphics.StrokeCap Butt;
enum_constant public static final androidx.compose.ui.graphics.StrokeCap Round;
@@ -860,14 +888,14 @@
method public void drawCircle-m-UMHxE(androidx.compose.ui.graphics.Brush brush, float radius, long center, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawImage-JUiai_k(androidx.compose.ui.graphics.ImageBitmap image, long topLeft, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawImage-Yc2aOMw(androidx.compose.ui.graphics.ImageBitmap image, long srcOffset, long srcSize, long dstOffset, long dstSize, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawLine-1-s4MmQ(long color, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawLine-IQGCKvc(androidx.compose.ui.graphics.Brush brush, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawLine-QXZmVdc(long color, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawLine-UXw4dv4(androidx.compose.ui.graphics.Brush brush, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawOval-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawOval-IdEHoqk(long color, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawPath(androidx.compose.ui.graphics.Path path, androidx.compose.ui.graphics.Brush brush, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawPath-tilSWAQ(androidx.compose.ui.graphics.Path path, long color, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawPoints-8s8raUw(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawPoints-Aqy9O-k(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawRect-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawRect-IdEHoqk(long color, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawRoundRect-fNghmuc(long color, long topLeft, long size, long cornerRadius, androidx.compose.ui.graphics.drawscope.DrawStyle style, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
@@ -906,14 +934,14 @@
method public void drawCircle-m-UMHxE(androidx.compose.ui.graphics.Brush brush, optional float radius, optional long center, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawImage-JUiai_k(androidx.compose.ui.graphics.ImageBitmap image, optional long topLeft, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawImage-Yc2aOMw(androidx.compose.ui.graphics.ImageBitmap image, optional long srcOffset, optional long srcSize, optional long dstOffset, optional long dstSize, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawLine-1-s4MmQ(long color, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawLine-IQGCKvc(androidx.compose.ui.graphics.Brush brush, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawLine-QXZmVdc(long color, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawLine-UXw4dv4(androidx.compose.ui.graphics.Brush brush, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawOval-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawOval-IdEHoqk(long color, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawPath(androidx.compose.ui.graphics.Path path, androidx.compose.ui.graphics.Brush brush, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawPath-tilSWAQ(androidx.compose.ui.graphics.Path path, long color, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawPoints-8s8raUw(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawPoints-Aqy9O-k(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawRect-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawRect-IdEHoqk(long color, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawRoundRect-fNghmuc(long color, optional long topLeft, optional long size, optional long cornerRadius, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
@@ -982,23 +1010,23 @@
}
public final class Stroke extends androidx.compose.ui.graphics.drawscope.DrawStyle {
- ctor public Stroke(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, android.graphics.PathEffect? pathEffect);
+ ctor public Stroke(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, androidx.compose.ui.graphics.PathEffect? pathEffect);
ctor public Stroke();
method public float component1();
method public float component2();
method public androidx.compose.ui.graphics.StrokeCap component3();
method public androidx.compose.ui.graphics.StrokeJoin component4();
- method public android.graphics.PathEffect? component5();
- method public androidx.compose.ui.graphics.drawscope.Stroke copy(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, android.graphics.PathEffect? pathEffect);
+ method public androidx.compose.ui.graphics.PathEffect? component5();
+ method public androidx.compose.ui.graphics.drawscope.Stroke copy(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, androidx.compose.ui.graphics.PathEffect? pathEffect);
method public androidx.compose.ui.graphics.StrokeCap getCap();
method public androidx.compose.ui.graphics.StrokeJoin getJoin();
method public float getMiter();
- method public android.graphics.PathEffect? getPathEffect();
+ method public androidx.compose.ui.graphics.PathEffect? getPathEffect();
method public float getWidth();
property public final androidx.compose.ui.graphics.StrokeCap cap;
property public final androidx.compose.ui.graphics.StrokeJoin join;
property public final float miter;
- property public final android.graphics.PathEffect? pathEffect;
+ property public final androidx.compose.ui.graphics.PathEffect? pathEffect;
property public final float width;
field public static final androidx.compose.ui.graphics.drawscope.Stroke.Companion Companion;
field public static final float DefaultMiter = 4.0f;
diff --git a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
index 2b56325..b18ab3f 100644
--- a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
@@ -31,6 +31,7 @@
method public androidx.compose.ui.graphics.ColorFilter? getColorFilter();
method public androidx.compose.ui.graphics.FilterQuality getFilterQuality();
method public android.graphics.PathEffect? getNativePathEffect();
+ method public androidx.compose.ui.graphics.PathEffect? getPathEffect();
method public android.graphics.Shader? getShader();
method public androidx.compose.ui.graphics.StrokeCap getStrokeCap();
method public androidx.compose.ui.graphics.StrokeJoin getStrokeJoin();
@@ -45,6 +46,7 @@
method public void setColorFilter(androidx.compose.ui.graphics.ColorFilter? value);
method public void setFilterQuality(androidx.compose.ui.graphics.FilterQuality value);
method public void setNativePathEffect(android.graphics.PathEffect? value);
+ method public void setPathEffect(androidx.compose.ui.graphics.PathEffect? value);
method public void setShader(android.graphics.Shader? value);
method public void setStrokeCap(androidx.compose.ui.graphics.StrokeCap value);
method public void setStrokeJoin(androidx.compose.ui.graphics.StrokeJoin value);
@@ -58,6 +60,7 @@
property public androidx.compose.ui.graphics.FilterQuality filterQuality;
property public boolean isAntiAlias;
property public android.graphics.PathEffect? nativePathEffect;
+ property public androidx.compose.ui.graphics.PathEffect? pathEffect;
property public android.graphics.Shader? shader;
property public androidx.compose.ui.graphics.StrokeCap strokeCap;
property public androidx.compose.ui.graphics.StrokeJoin strokeJoin;
@@ -104,6 +107,11 @@
property public boolean isEmpty;
}
+ public final class AndroidPathEffectKt {
+ method public static android.graphics.PathEffect asAndroidPathEffect(androidx.compose.ui.graphics.PathEffect);
+ method public static androidx.compose.ui.graphics.PathEffect toComposePathEffect(android.graphics.PathEffect);
+ }
+
public final class AndroidPathKt {
method public static androidx.compose.ui.graphics.Path Path();
method public static inline android.graphics.Path asAndroidPath(androidx.compose.ui.graphics.Path);
@@ -430,7 +438,8 @@
method public long getColor-0d7_KjU();
method public androidx.compose.ui.graphics.ColorFilter? getColorFilter();
method public androidx.compose.ui.graphics.FilterQuality getFilterQuality();
- method public android.graphics.PathEffect? getNativePathEffect();
+ method @Deprecated public android.graphics.PathEffect? getNativePathEffect();
+ method public androidx.compose.ui.graphics.PathEffect? getPathEffect();
method public android.graphics.Shader? getShader();
method public androidx.compose.ui.graphics.StrokeCap getStrokeCap();
method public androidx.compose.ui.graphics.StrokeJoin getStrokeJoin();
@@ -444,7 +453,8 @@
method public void setColor-8_81llA(long p);
method public void setColorFilter(androidx.compose.ui.graphics.ColorFilter? p);
method public void setFilterQuality(androidx.compose.ui.graphics.FilterQuality p);
- method public void setNativePathEffect(android.graphics.PathEffect? p);
+ method @Deprecated public void setNativePathEffect(android.graphics.PathEffect? p);
+ method public void setPathEffect(androidx.compose.ui.graphics.PathEffect? p);
method public void setShader(android.graphics.Shader? p);
method public void setStrokeCap(androidx.compose.ui.graphics.StrokeCap p);
method public void setStrokeJoin(androidx.compose.ui.graphics.StrokeJoin p);
@@ -457,7 +467,8 @@
property public abstract androidx.compose.ui.graphics.ColorFilter? colorFilter;
property public abstract androidx.compose.ui.graphics.FilterQuality filterQuality;
property public abstract boolean isAntiAlias;
- property public abstract android.graphics.PathEffect? nativePathEffect;
+ property @Deprecated public abstract android.graphics.PathEffect? nativePathEffect;
+ property public abstract androidx.compose.ui.graphics.PathEffect? pathEffect;
property public abstract android.graphics.Shader? shader;
property public abstract androidx.compose.ui.graphics.StrokeCap strokeCap;
property public abstract androidx.compose.ui.graphics.StrokeJoin strokeJoin;
@@ -511,6 +522,17 @@
method public androidx.compose.ui.graphics.Path combine(androidx.compose.ui.graphics.PathOperation operation, androidx.compose.ui.graphics.Path path1, androidx.compose.ui.graphics.Path path2);
}
+ public interface PathEffect {
+ field public static final androidx.compose.ui.graphics.PathEffect.Companion Companion;
+ }
+
+ public static final class PathEffect.Companion {
+ method public androidx.compose.ui.graphics.PathEffect chainPathEffect(androidx.compose.ui.graphics.PathEffect outer, androidx.compose.ui.graphics.PathEffect inner);
+ method public androidx.compose.ui.graphics.PathEffect cornerPathEffect(float radius);
+ method public androidx.compose.ui.graphics.PathEffect dashPathEffect(float[] intervals, optional float phase);
+ method public androidx.compose.ui.graphics.PathEffect stampedPathEffect(androidx.compose.ui.graphics.Path shape, float advance, float phase, androidx.compose.ui.graphics.StampedPathEffectStyle style);
+ }
+
public enum PathFillType {
enum_constant public static final androidx.compose.ui.graphics.PathFillType EvenOdd;
enum_constant public static final androidx.compose.ui.graphics.PathFillType NonZero;
@@ -612,6 +634,12 @@
property public final long value;
}
+ public enum StampedPathEffectStyle {
+ enum_constant public static final androidx.compose.ui.graphics.StampedPathEffectStyle Morph;
+ enum_constant public static final androidx.compose.ui.graphics.StampedPathEffectStyle Rotate;
+ enum_constant public static final androidx.compose.ui.graphics.StampedPathEffectStyle Translate;
+ }
+
public enum StrokeCap {
enum_constant public static final androidx.compose.ui.graphics.StrokeCap Butt;
enum_constant public static final androidx.compose.ui.graphics.StrokeCap Round;
@@ -860,14 +888,14 @@
method public void drawCircle-m-UMHxE(androidx.compose.ui.graphics.Brush brush, float radius, long center, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawImage-JUiai_k(androidx.compose.ui.graphics.ImageBitmap image, long topLeft, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawImage-Yc2aOMw(androidx.compose.ui.graphics.ImageBitmap image, long srcOffset, long srcSize, long dstOffset, long dstSize, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawLine-1-s4MmQ(long color, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawLine-IQGCKvc(androidx.compose.ui.graphics.Brush brush, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawLine-QXZmVdc(long color, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawLine-UXw4dv4(androidx.compose.ui.graphics.Brush brush, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawOval-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawOval-IdEHoqk(long color, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawPath(androidx.compose.ui.graphics.Path path, androidx.compose.ui.graphics.Brush brush, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawPath-tilSWAQ(androidx.compose.ui.graphics.Path path, long color, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawPoints-8s8raUw(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawPoints-Aqy9O-k(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawRect-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawRect-IdEHoqk(long color, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawRoundRect-fNghmuc(long color, long topLeft, long size, long cornerRadius, androidx.compose.ui.graphics.drawscope.DrawStyle style, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
@@ -906,14 +934,14 @@
method public void drawCircle-m-UMHxE(androidx.compose.ui.graphics.Brush brush, optional float radius, optional long center, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawImage-JUiai_k(androidx.compose.ui.graphics.ImageBitmap image, optional long topLeft, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawImage-Yc2aOMw(androidx.compose.ui.graphics.ImageBitmap image, optional long srcOffset, optional long srcSize, optional long dstOffset, optional long dstSize, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawLine-1-s4MmQ(long color, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawLine-IQGCKvc(androidx.compose.ui.graphics.Brush brush, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawLine-QXZmVdc(long color, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawLine-UXw4dv4(androidx.compose.ui.graphics.Brush brush, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawOval-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawOval-IdEHoqk(long color, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawPath(androidx.compose.ui.graphics.Path path, androidx.compose.ui.graphics.Brush brush, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawPath-tilSWAQ(androidx.compose.ui.graphics.Path path, long color, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawPoints-8s8raUw(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawPoints-Aqy9O-k(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawRect-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawRect-IdEHoqk(long color, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawRoundRect-fNghmuc(long color, optional long topLeft, optional long size, optional long cornerRadius, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
@@ -982,23 +1010,23 @@
}
public final class Stroke extends androidx.compose.ui.graphics.drawscope.DrawStyle {
- ctor public Stroke(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, android.graphics.PathEffect? pathEffect);
+ ctor public Stroke(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, androidx.compose.ui.graphics.PathEffect? pathEffect);
ctor public Stroke();
method public float component1();
method public float component2();
method public androidx.compose.ui.graphics.StrokeCap component3();
method public androidx.compose.ui.graphics.StrokeJoin component4();
- method public android.graphics.PathEffect? component5();
- method public androidx.compose.ui.graphics.drawscope.Stroke copy(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, android.graphics.PathEffect? pathEffect);
+ method public androidx.compose.ui.graphics.PathEffect? component5();
+ method public androidx.compose.ui.graphics.drawscope.Stroke copy(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, androidx.compose.ui.graphics.PathEffect? pathEffect);
method public androidx.compose.ui.graphics.StrokeCap getCap();
method public androidx.compose.ui.graphics.StrokeJoin getJoin();
method public float getMiter();
- method public android.graphics.PathEffect? getPathEffect();
+ method public androidx.compose.ui.graphics.PathEffect? getPathEffect();
method public float getWidth();
property public final androidx.compose.ui.graphics.StrokeCap cap;
property public final androidx.compose.ui.graphics.StrokeJoin join;
property public final float miter;
- property public final android.graphics.PathEffect? pathEffect;
+ property public final androidx.compose.ui.graphics.PathEffect? pathEffect;
property public final float width;
field public static final androidx.compose.ui.graphics.drawscope.Stroke.Companion Companion;
field public static final float DefaultMiter = 4.0f;
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index f6b1064..acc2da8 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -61,6 +61,7 @@
method public androidx.compose.ui.graphics.ColorFilter? getColorFilter();
method public androidx.compose.ui.graphics.FilterQuality getFilterQuality();
method public android.graphics.PathEffect? getNativePathEffect();
+ method public androidx.compose.ui.graphics.PathEffect? getPathEffect();
method public android.graphics.Shader? getShader();
method public androidx.compose.ui.graphics.StrokeCap getStrokeCap();
method public androidx.compose.ui.graphics.StrokeJoin getStrokeJoin();
@@ -75,6 +76,7 @@
method public void setColorFilter(androidx.compose.ui.graphics.ColorFilter? value);
method public void setFilterQuality(androidx.compose.ui.graphics.FilterQuality value);
method public void setNativePathEffect(android.graphics.PathEffect? value);
+ method public void setPathEffect(androidx.compose.ui.graphics.PathEffect? value);
method public void setShader(android.graphics.Shader? value);
method public void setStrokeCap(androidx.compose.ui.graphics.StrokeCap value);
method public void setStrokeJoin(androidx.compose.ui.graphics.StrokeJoin value);
@@ -88,6 +90,7 @@
property public androidx.compose.ui.graphics.FilterQuality filterQuality;
property public boolean isAntiAlias;
property public android.graphics.PathEffect? nativePathEffect;
+ property public androidx.compose.ui.graphics.PathEffect? pathEffect;
property public android.graphics.Shader? shader;
property public androidx.compose.ui.graphics.StrokeCap strokeCap;
property public androidx.compose.ui.graphics.StrokeJoin strokeJoin;
@@ -134,6 +137,11 @@
property public boolean isEmpty;
}
+ public final class AndroidPathEffectKt {
+ method public static android.graphics.PathEffect asAndroidPathEffect(androidx.compose.ui.graphics.PathEffect);
+ method public static androidx.compose.ui.graphics.PathEffect toComposePathEffect(android.graphics.PathEffect);
+ }
+
public final class AndroidPathKt {
method public static androidx.compose.ui.graphics.Path Path();
method public static inline android.graphics.Path asAndroidPath(androidx.compose.ui.graphics.Path);
@@ -462,7 +470,8 @@
method public long getColor-0d7_KjU();
method public androidx.compose.ui.graphics.ColorFilter? getColorFilter();
method public androidx.compose.ui.graphics.FilterQuality getFilterQuality();
- method public android.graphics.PathEffect? getNativePathEffect();
+ method @Deprecated public android.graphics.PathEffect? getNativePathEffect();
+ method public androidx.compose.ui.graphics.PathEffect? getPathEffect();
method public android.graphics.Shader? getShader();
method public androidx.compose.ui.graphics.StrokeCap getStrokeCap();
method public androidx.compose.ui.graphics.StrokeJoin getStrokeJoin();
@@ -476,7 +485,8 @@
method public void setColor-8_81llA(long p);
method public void setColorFilter(androidx.compose.ui.graphics.ColorFilter? p);
method public void setFilterQuality(androidx.compose.ui.graphics.FilterQuality p);
- method public void setNativePathEffect(android.graphics.PathEffect? p);
+ method @Deprecated public void setNativePathEffect(android.graphics.PathEffect? p);
+ method public void setPathEffect(androidx.compose.ui.graphics.PathEffect? p);
method public void setShader(android.graphics.Shader? p);
method public void setStrokeCap(androidx.compose.ui.graphics.StrokeCap p);
method public void setStrokeJoin(androidx.compose.ui.graphics.StrokeJoin p);
@@ -489,7 +499,8 @@
property public abstract androidx.compose.ui.graphics.ColorFilter? colorFilter;
property public abstract androidx.compose.ui.graphics.FilterQuality filterQuality;
property public abstract boolean isAntiAlias;
- property public abstract android.graphics.PathEffect? nativePathEffect;
+ property @Deprecated public abstract android.graphics.PathEffect? nativePathEffect;
+ property public abstract androidx.compose.ui.graphics.PathEffect? pathEffect;
property public abstract android.graphics.Shader? shader;
property public abstract androidx.compose.ui.graphics.StrokeCap strokeCap;
property public abstract androidx.compose.ui.graphics.StrokeJoin strokeJoin;
@@ -543,6 +554,17 @@
method public androidx.compose.ui.graphics.Path combine(androidx.compose.ui.graphics.PathOperation operation, androidx.compose.ui.graphics.Path path1, androidx.compose.ui.graphics.Path path2);
}
+ public interface PathEffect {
+ field public static final androidx.compose.ui.graphics.PathEffect.Companion Companion;
+ }
+
+ public static final class PathEffect.Companion {
+ method public androidx.compose.ui.graphics.PathEffect chainPathEffect(androidx.compose.ui.graphics.PathEffect outer, androidx.compose.ui.graphics.PathEffect inner);
+ method public androidx.compose.ui.graphics.PathEffect cornerPathEffect(float radius);
+ method public androidx.compose.ui.graphics.PathEffect dashPathEffect(float[] intervals, optional float phase);
+ method public androidx.compose.ui.graphics.PathEffect stampedPathEffect(androidx.compose.ui.graphics.Path shape, float advance, float phase, androidx.compose.ui.graphics.StampedPathEffectStyle style);
+ }
+
public enum PathFillType {
enum_constant public static final androidx.compose.ui.graphics.PathFillType EvenOdd;
enum_constant public static final androidx.compose.ui.graphics.PathFillType NonZero;
@@ -644,6 +666,12 @@
property public final long value;
}
+ public enum StampedPathEffectStyle {
+ enum_constant public static final androidx.compose.ui.graphics.StampedPathEffectStyle Morph;
+ enum_constant public static final androidx.compose.ui.graphics.StampedPathEffectStyle Rotate;
+ enum_constant public static final androidx.compose.ui.graphics.StampedPathEffectStyle Translate;
+ }
+
public enum StrokeCap {
enum_constant public static final androidx.compose.ui.graphics.StrokeCap Butt;
enum_constant public static final androidx.compose.ui.graphics.StrokeCap Round;
@@ -892,14 +920,14 @@
method public void drawCircle-m-UMHxE(androidx.compose.ui.graphics.Brush brush, float radius, long center, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawImage-JUiai_k(androidx.compose.ui.graphics.ImageBitmap image, long topLeft, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawImage-Yc2aOMw(androidx.compose.ui.graphics.ImageBitmap image, long srcOffset, long srcSize, long dstOffset, long dstSize, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawLine-1-s4MmQ(long color, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawLine-IQGCKvc(androidx.compose.ui.graphics.Brush brush, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawLine-QXZmVdc(long color, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawLine-UXw4dv4(androidx.compose.ui.graphics.Brush brush, long start, long end, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawOval-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawOval-IdEHoqk(long color, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawPath(androidx.compose.ui.graphics.Path path, androidx.compose.ui.graphics.Brush brush, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawPath-tilSWAQ(androidx.compose.ui.graphics.Path path, long color, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawPoints-8s8raUw(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, android.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawPoints-Aqy9O-k(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, float strokeWidth, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.PathEffect? pathEffect, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawRect-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawRect-IdEHoqk(long color, long topLeft, long size, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.drawscope.DrawStyle style, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawRoundRect-fNghmuc(long color, long topLeft, long size, long cornerRadius, androidx.compose.ui.graphics.drawscope.DrawStyle style, @FloatRange(from=0.0, to=1.0) float alpha, androidx.compose.ui.graphics.ColorFilter? colorFilter, androidx.compose.ui.graphics.BlendMode blendMode);
@@ -962,14 +990,14 @@
method public void drawCircle-m-UMHxE(androidx.compose.ui.graphics.Brush brush, optional float radius, optional long center, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawImage-JUiai_k(androidx.compose.ui.graphics.ImageBitmap image, optional long topLeft, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawImage-Yc2aOMw(androidx.compose.ui.graphics.ImageBitmap image, optional long srcOffset, optional long srcSize, optional long dstOffset, optional long dstSize, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawLine-1-s4MmQ(long color, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawLine-IQGCKvc(androidx.compose.ui.graphics.Brush brush, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawLine-QXZmVdc(long color, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawLine-UXw4dv4(androidx.compose.ui.graphics.Brush brush, long start, long end, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawOval-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawOval-IdEHoqk(long color, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawPath(androidx.compose.ui.graphics.Path path, androidx.compose.ui.graphics.Brush brush, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawPath-tilSWAQ(androidx.compose.ui.graphics.Path path, long color, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
- method public void drawPoints-8s8raUw(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional android.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawPoints(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, androidx.compose.ui.graphics.Brush brush, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
+ method public void drawPoints-Aqy9O-k(java.util.List<androidx.compose.ui.geometry.Offset> points, androidx.compose.ui.graphics.PointMode pointMode, long color, optional float strokeWidth, optional androidx.compose.ui.graphics.StrokeCap cap, optional androidx.compose.ui.graphics.PathEffect? pathEffect, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawRect-0a6MmAQ(androidx.compose.ui.graphics.Brush brush, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawRect-IdEHoqk(long color, optional long topLeft, optional long size, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
method public void drawRoundRect-fNghmuc(long color, optional long topLeft, optional long size, optional long cornerRadius, optional androidx.compose.ui.graphics.drawscope.DrawStyle style, optional @FloatRange(from=0.0, to=1.0) float alpha, optional androidx.compose.ui.graphics.ColorFilter? colorFilter, optional androidx.compose.ui.graphics.BlendMode blendMode);
@@ -1038,23 +1066,23 @@
}
public final class Stroke extends androidx.compose.ui.graphics.drawscope.DrawStyle {
- ctor public Stroke(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, android.graphics.PathEffect? pathEffect);
+ ctor public Stroke(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, androidx.compose.ui.graphics.PathEffect? pathEffect);
ctor public Stroke();
method public float component1();
method public float component2();
method public androidx.compose.ui.graphics.StrokeCap component3();
method public androidx.compose.ui.graphics.StrokeJoin component4();
- method public android.graphics.PathEffect? component5();
- method public androidx.compose.ui.graphics.drawscope.Stroke copy(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, android.graphics.PathEffect? pathEffect);
+ method public androidx.compose.ui.graphics.PathEffect? component5();
+ method public androidx.compose.ui.graphics.drawscope.Stroke copy(float width, float miter, androidx.compose.ui.graphics.StrokeCap cap, androidx.compose.ui.graphics.StrokeJoin join, androidx.compose.ui.graphics.PathEffect? pathEffect);
method public androidx.compose.ui.graphics.StrokeCap getCap();
method public androidx.compose.ui.graphics.StrokeJoin getJoin();
method public float getMiter();
- method public android.graphics.PathEffect? getPathEffect();
+ method public androidx.compose.ui.graphics.PathEffect? getPathEffect();
method public float getWidth();
property public final androidx.compose.ui.graphics.StrokeCap cap;
property public final androidx.compose.ui.graphics.StrokeJoin join;
property public final float miter;
- property public final android.graphics.PathEffect? pathEffect;
+ property public final androidx.compose.ui.graphics.PathEffect? pathEffect;
property public final float width;
field public static final androidx.compose.ui.graphics.drawscope.Stroke.Companion Companion;
field public static final float DefaultMiter = 4.0f;
diff --git a/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/PathEffectSample.kt b/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/PathEffectSample.kt
new file mode 100644
index 0000000..09c5dcc
--- /dev/null
+++ b/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/PathEffectSample.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.compose.ui.graphics.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+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.graphics.Path
+import androidx.compose.ui.graphics.PathEffect
+import androidx.compose.ui.graphics.StampedPathEffectStyle
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.unit.dp
+
+@Sampled
+@Composable
+fun StampedPathEffectSample() {
+ val size = 20f
+ val square = Path().apply {
+ lineTo(size, 0f)
+ lineTo(size, size)
+ lineTo(0f, size)
+ close()
+ }
+ Column(modifier = Modifier.fillMaxHeight().wrapContentSize(Alignment.Center)) {
+ val canvasModifier = Modifier.size(80.dp).align(Alignment.CenterHorizontally)
+
+ // StampedPathEffectStyle.Morph will modify the lines of the square to be curved to fit
+ // the curvature of the circle itself. Each stamped square will be rendered as an arc
+ // that is fully contained by the bounds of the circle itself
+ Canvas(modifier = canvasModifier) {
+ drawCircle(color = Color.Blue)
+ drawCircle(
+ color = Color.Red,
+ style = Stroke(
+ pathEffect = PathEffect.stampedPathEffect(
+ shape = square,
+ style = StampedPathEffectStyle.Morph,
+ phase = 0f,
+ advance = 30f
+ )
+ )
+ )
+ }
+
+ Spacer(modifier = Modifier.size(10.dp))
+
+ // StampedPathEffectStyle.Rotate will draw the square repeatedly around the circle
+ // such that each stamped square is centered on the circumference of the circle and is
+ // rotated along the curvature of the circle itself
+ Canvas(modifier = canvasModifier) {
+ drawCircle(color = Color.Blue)
+ drawCircle(
+ color = Color.Red,
+ style = Stroke(
+ pathEffect = PathEffect.stampedPathEffect(
+ shape = square,
+ style = StampedPathEffectStyle.Rotate,
+ phase = 0f,
+ advance = 30f
+ )
+ )
+ )
+ }
+
+ Spacer(modifier = Modifier.size(10.dp))
+
+ // StampedPathEffectStyle.Translate will draw the square repeatedly around the circle
+ // with the top left of each stamped square on the circumference of the circle
+ Canvas(modifier = canvasModifier) {
+ drawCircle(color = Color.Blue)
+ drawCircle(
+ color = Color.Red,
+ style = Stroke(
+ pathEffect = PathEffect.stampedPathEffect(
+ shape = square,
+ style = StampedPathEffectStyle.Translate,
+ phase = 0f,
+ advance = 30f
+ )
+ )
+ )
+ }
+ }
+}
\ No newline at end of file
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 ecec048..c5bb9d6 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
@@ -17,8 +17,8 @@
package androidx.compose.ui.graphics
import android.content.Context
+import android.graphics.Bitmap
import android.graphics.Canvas
-import android.graphics.Color
import android.os.Build
import android.util.AttributeSet
import android.view.Gravity
@@ -61,14 +61,14 @@
activityTestRule.runOnUiThread {
val group = EnableDisableZViewGroup(drawLatch, activity)
groupView = group
- group.setBackgroundColor(Color.WHITE)
+ group.setBackgroundColor(android.graphics.Color.WHITE)
group.layoutParams = ViewGroup.LayoutParams(12, 12)
val child = View(activity)
val childLayoutParams = FrameLayout.LayoutParams(10, 10)
childLayoutParams.gravity = Gravity.TOP or Gravity.LEFT
child.layoutParams = childLayoutParams
child.elevation = 4f
- child.setBackgroundColor(Color.WHITE)
+ child.setBackgroundColor(android.graphics.Color.WHITE)
group.addView(child)
activity.setContentView(group)
}
@@ -78,9 +78,9 @@
// the drawn content can get onto the screen before we capture the bitmap.
activityTestRule.runOnUiThread { }
val bitmap = groupView!!.captureToImage().asAndroidBitmap()
- assertEquals(Color.WHITE, bitmap.getPixel(0, 0))
- assertEquals(Color.WHITE, bitmap.getPixel(9, 9))
- assertNotEquals(Color.WHITE, bitmap.getPixel(10, 10))
+ assertEquals(android.graphics.Color.WHITE, bitmap.getPixel(0, 0))
+ assertEquals(android.graphics.Color.WHITE, bitmap.getPixel(9, 9))
+ assertNotEquals(android.graphics.Color.WHITE, bitmap.getPixel(10, 10))
}
@Test
@@ -266,6 +266,215 @@
assertEquals(bg, pixelMap[75, 76])
}
+ @Test
+ fun testCornerPathEffect() {
+ val width = 80
+ val height = 80
+ val radius = 20f
+ val imageBitmap = ImageBitmap(width, height)
+ imageBitmap.asAndroidBitmap().eraseColor(android.graphics.Color.WHITE)
+ val canvas = Canvas(imageBitmap)
+ canvas.drawRect(
+ 0f,
+ 0f,
+ width.toFloat(),
+ height.toFloat(),
+ Paint().apply {
+ color = Color.Blue
+ pathEffect = PathEffect.cornerPathEffect(radius)
+ }
+ )
+
+ val androidBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ androidBitmap.eraseColor(android.graphics.Color.WHITE)
+ val androidCanvas = android.graphics.Canvas(androidBitmap)
+ androidCanvas.drawRect(
+ 0f,
+ 0f,
+ width.toFloat(),
+ height.toFloat(),
+ android.graphics.Paint().apply {
+ isAntiAlias = true
+ color = android.graphics.Color.BLUE
+ pathEffect = android.graphics.CornerPathEffect(radius)
+ }
+ )
+
+ val composePixels = imageBitmap.toPixelMap()
+ for (i in 0 until 80) {
+ for (j in 0 until 80) {
+ assertEquals(
+ "invalid color at i: " + i + ", " + j,
+ composePixels[i, j].toArgb(),
+ androidBitmap.getPixel(i, j)
+ )
+ }
+ }
+ }
+
+ @Test
+ fun testDashPathEffect() {
+ val width = 80
+ val height = 80
+ val imageBitmap = ImageBitmap(width, height)
+ imageBitmap.asAndroidBitmap().eraseColor(android.graphics.Color.WHITE)
+ val canvas = Canvas(imageBitmap)
+ canvas.drawRect(
+ 0f,
+ 0f,
+ width.toFloat(),
+ height.toFloat(),
+ Paint().apply {
+ color = Color.Blue
+ pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 5f), 8f)
+ }
+ )
+
+ val androidBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ androidBitmap.eraseColor(android.graphics.Color.WHITE)
+ val androidCanvas = android.graphics.Canvas(androidBitmap)
+ androidCanvas.drawRect(
+ 0f,
+ 0f,
+ width.toFloat(),
+ height.toFloat(),
+ android.graphics.Paint().apply {
+ isAntiAlias = true
+ color = android.graphics.Color.BLUE
+ pathEffect = android.graphics.DashPathEffect(floatArrayOf(10f, 5f), 8f)
+ }
+ )
+
+ val composePixels = imageBitmap.toPixelMap()
+ for (i in 0 until 80) {
+ for (j in 0 until 80) {
+ assertEquals(
+ "invalid color at i: " + i + ", " + j,
+ composePixels[i, j].toArgb(),
+ androidBitmap.getPixel(i, j)
+ )
+ }
+ }
+ }
+
+ @Test
+ fun testChainPathEffect() {
+ val width = 80
+ val height = 80
+ val imageBitmap = ImageBitmap(width, height)
+ imageBitmap.asAndroidBitmap().eraseColor(android.graphics.Color.WHITE)
+ val canvas = Canvas(imageBitmap)
+ canvas.drawRect(
+ 0f,
+ 0f,
+ width.toFloat(),
+ height.toFloat(),
+ Paint().apply {
+ color = Color.Blue
+ pathEffect =
+ PathEffect.chainPathEffect(
+ PathEffect.dashPathEffect(floatArrayOf(10f, 5f), 8f),
+ PathEffect.cornerPathEffect(20f)
+ )
+ }
+ )
+
+ val androidBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ androidBitmap.eraseColor(android.graphics.Color.WHITE)
+ val androidCanvas = android.graphics.Canvas(androidBitmap)
+ androidCanvas.drawRect(
+ 0f,
+ 0f,
+ width.toFloat(),
+ height.toFloat(),
+ android.graphics.Paint().apply {
+ isAntiAlias = true
+ color = android.graphics.Color.BLUE
+ pathEffect =
+ android.graphics.ComposePathEffect(
+ android.graphics.DashPathEffect(floatArrayOf(10f, 5f), 8f),
+ android.graphics.CornerPathEffect(20f)
+ )
+ }
+ )
+
+ val composePixels = imageBitmap.toPixelMap()
+ for (i in 0 until 80) {
+ for (j in 0 until 80) {
+ assertEquals(
+ "invalid color at i: " + i + ", " + j,
+ composePixels[i, j].toArgb(),
+ androidBitmap.getPixel(i, j)
+ )
+ }
+ }
+ }
+
+ @Test
+ fun testPathDashPathEffect() {
+ val width = 80
+ val height = 80
+ val imageBitmap = ImageBitmap(width, height)
+ imageBitmap.asAndroidBitmap().eraseColor(android.graphics.Color.WHITE)
+ val canvas = Canvas(imageBitmap)
+ canvas.drawRect(
+ 0f,
+ 0f,
+ width.toFloat(),
+ height.toFloat(),
+ Paint().apply {
+ color = Color.Blue
+ pathEffect =
+ PathEffect.stampedPathEffect(
+ Path().apply {
+ lineTo(0f, 5f)
+ lineTo(5f, 5f)
+ close()
+ },
+ 5f,
+ 2f,
+ StampedPathEffectStyle.Rotate
+ )
+ }
+ )
+
+ val androidBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ androidBitmap.eraseColor(android.graphics.Color.WHITE)
+ val androidCanvas = android.graphics.Canvas(androidBitmap)
+ androidCanvas.drawRect(
+ 0f,
+ 0f,
+ width.toFloat(),
+ height.toFloat(),
+ android.graphics.Paint().apply {
+ isAntiAlias = true
+ color = android.graphics.Color.BLUE
+ pathEffect =
+ android.graphics.PathDashPathEffect(
+ android.graphics.Path().apply {
+ lineTo(0f, 5f)
+ lineTo(5f, 5f)
+ close()
+ },
+ 5f,
+ 2f,
+ android.graphics.PathDashPathEffect.Style.ROTATE
+ )
+ }
+ )
+
+ val composePixels = imageBitmap.toPixelMap()
+ for (i in 0 until 80) {
+ for (j in 0 until 80) {
+ assertEquals(
+ "invalid color at i: " + i + ", " + j,
+ composePixels[i, j].toArgb(),
+ androidBitmap.getPixel(i, j)
+ )
+ }
+ }
+ }
+
class EnableDisableZViewGroup @JvmOverloads constructor(
val drawLatch: CountDownLatch,
context: Context,
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPaint.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPaint.kt
index 2eca0e3..30b33a1 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPaint.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPaint.kt
@@ -108,7 +108,17 @@
internalPaint.setNativeColorFilter(value)
}
- override var nativePathEffect: NativePathEffect? = null
+ override var nativePathEffect: NativePathEffect?
+ get() = pathEffect?.asAndroidPathEffect()
+ set(value) {
+ pathEffect = if (value == null) {
+ null
+ } else {
+ AndroidPathEffect(value)
+ }
+ }
+
+ override var pathEffect: PathEffect? = null
set(value) {
internalPaint.setNativePathEffect(value)
field = value
@@ -235,6 +245,6 @@
this.shader = value
}
-internal fun NativePaint.setNativePathEffect(value: NativePathEffect?) {
- this.pathEffect = value
+internal fun NativePaint.setNativePathEffect(value: PathEffect?) {
+ this.pathEffect = (value as AndroidPathEffect).nativePathEffect
}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathEffect.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathEffect.kt
new file mode 100644
index 0000000..4755271
--- /dev/null
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPathEffect.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.graphics
+
+import android.graphics.PathDashPathEffect
+
+/**
+ * Obtain a reference to the Android PathEffect type
+ */
+internal class AndroidPathEffect(val nativePathEffect: android.graphics.PathEffect) : PathEffect
+
+fun PathEffect.asAndroidPathEffect(): android.graphics.PathEffect =
+ (this as AndroidPathEffect).nativePathEffect
+
+fun android.graphics.PathEffect.toComposePathEffect(): PathEffect = AndroidPathEffect(this)
+
+internal actual fun actualCornerPathEffect(radius: Float): PathEffect =
+ AndroidPathEffect(android.graphics.CornerPathEffect(radius))
+
+internal actual fun actualDashPathEffect(intervals: FloatArray, phase: Float): PathEffect =
+ AndroidPathEffect(android.graphics.DashPathEffect(intervals, phase))
+
+internal actual fun actualChainPathEffect(outer: PathEffect, inner: PathEffect): PathEffect =
+ AndroidPathEffect(
+ android.graphics.ComposePathEffect(
+ (outer as AndroidPathEffect).nativePathEffect,
+ (inner as AndroidPathEffect).nativePathEffect
+ )
+ )
+
+internal actual fun actualStampedPathEffect(
+ shape: Path,
+ advance: Float,
+ phase: Float,
+ style: StampedPathEffectStyle
+): PathEffect =
+ AndroidPathEffect(
+ PathDashPathEffect(
+ shape.asAndroidPath(),
+ advance,
+ phase,
+ style.toAndroidPathDashPathEffectStyle()
+ )
+ )
+
+internal fun StampedPathEffectStyle.toAndroidPathDashPathEffectStyle() =
+ when (this) {
+ StampedPathEffectStyle.Morph -> PathDashPathEffect.Style.MORPH
+ StampedPathEffectStyle.Rotate -> PathDashPathEffect.Style.ROTATE
+ StampedPathEffectStyle.Translate -> PathDashPathEffect.Style.TRANSLATE
+ }
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Paint.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Paint.kt
index bdbd687..32a3905 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Paint.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Paint.kt
@@ -28,111 +28,126 @@
interface Paint {
fun asFrameworkPaint(): NativePaint
+ /**
+ * Configures the alpha value between 0f to 1f representing fully transparent to fully
+ * opaque for the color drawn with this Paint
+ */
var alpha: Float
- // Whether to apply anti-aliasing to lines and images drawn on the
- // canvas.
- //
- // Defaults to true.
+ /**
+ * Whether to apply anti-aliasing to lines and images drawn on the
+ * canvas.
+ * Defaults to true.
+ */
var isAntiAlias: Boolean
- // The color to use when stroking or filling a shape.
- //
- // Defaults to opaque black.
- //
- // See also:
- //
- // * [style], which controls whether to stroke or fill (or both).
- // * [colorFilter], which overrides [color].
- // * [shader], which overrides [color] with more elaborate effects.
- //
- // This color is not used when compositing. To colorize a layer, use
- // [colorFilter].
+ /**
+ * The color to use when stroking or filling a shape.
+ * Defaults to opaque black.
+ * See also:
+ * [style], which controls whether to stroke or fill (or both).
+ * [colorFilter], which overrides [color].
+ * [shader], which overrides [color] with more elaborate effects.
+ * This color is not used when compositing. To colorize a layer, use [colorFilter].
+ */
var color: Color
- // A blend mode to apply when a shape is drawn or a layer is composited.
- //
- // The source colors are from the shape being drawn (e.g. from
- // [Canvas.drawPath]) or layer being composited (the graphics that were drawn
- // between the [Canvas.saveLayer] and [Canvas.restore] calls), after applying
- // the [colorFilter], if any.
- //
- // The destination colors are from the background onto which the shape or
- // layer is being composited.
- //
- // Defaults to [BlendMode.srcOver].
- //
- // See also:
- //
- // * [Canvas.saveLayer], which uses its [Paint]'s [blendMode] to composite
- // the layer when [restore] is called.
- // * [BlendMode], which discusses the user of [saveLayer] with [blendMode].
+ /**
+ * A blend mode to apply when a shape is drawn or a layer is composited.
+ * The source colors are from the shape being drawn (e.g. from
+ * [Canvas.drawPath]) or layer being composited (the graphics that were drawn
+ * between the [Canvas.saveLayer] and [Canvas.restore] calls), after applying
+ * the [colorFilter], if any.
+ * The destination colors are from the background onto which the shape or
+ * layer is being composited.
+ * Defaults to [BlendMode.SrcOver].
+ * See also:
+ * [Canvas.saveLayer], which uses its [Paint]'s [blendMode] to composite
+ * the layer when [Canvas.restore] is called.
+ * [BlendMode], which discusses the user of [Canvas.saveLayer] with [blendMode].
+ */
var blendMode: BlendMode
- // Whether to paint inside shapes, the edges of shapes, or both.
- //
- // Defaults to [PaintingStyle.fill].
+ /**
+ * Whether to paint inside shapes, the edges of shapes, or both.
+ * Defaults to [PaintingStyle.Fill].
+ */
var style: PaintingStyle
- // How wide to make edges drawn when [style] is set to
- // [PaintingStyle.stroke]. The width is given in logical pixels measured in
- // the direction orthogonal to the direction of the path.
- //
- // Defaults to 0.0, which correspond to a hairline width.
+ /**
+ * How wide to make edges drawn when [style] is set to
+ * [PaintingStyle.Stroke]. The width is given in logical pixels measured in
+ * the direction orthogonal to the direction of the path.
+ * Defaults to 0.0, which correspond to a hairline width.
+ */
var strokeWidth: Float
- // The kind of finish to place on the end of lines drawn when
- // [style] is set to [PaintingStyle.stroke].
- //
- // Defaults to [StrokeCap.butt], i.e. no caps.
+ /**
+ * The kind of finish to place on the end of lines drawn when
+ * [style] is set to [PaintingStyle.Stroke].
+ * Defaults to [StrokeCap.Butt], i.e. no caps.
+ */
var strokeCap: StrokeCap
- // The kind of finish to place on the joins between segments.
- //
- // This applies to paths drawn when [style] is set to [PaintingStyle.stroke],
- // It does not apply to points drawn as lines with [Canvas.drawPoints].
- //
- // Defaults to [StrokeJoin.miter], i.e. sharp corners. See also
- // [strokeMiterLimit] to control when miters are replaced by bevels.
+ /**
+ * The kind of finish to place on the joins between segments.
+ * This applies to paths drawn when [style] is set to [PaintingStyle.Stroke],
+ * It does not apply to points drawn as lines with [Canvas.drawPoints].
+ * Defaults to [StrokeJoin.Miter], i.e. sharp corners. See also
+ * [strokeMiterLimit] to control when miters are replaced by bevels.
+ */
var strokeJoin: StrokeJoin
- // The limit for miters to be drawn on segments when the join is set to
- // [StrokeJoin.miter] and the [style] is set to [PaintingStyle.stroke]. If
- // this limit is exceeded, then a [StrokeJoin.bevel] join will be drawn
- // instead. This may cause some 'popping' of the corners of a path if the
- // angle between line segments is animated.
- //
- // This limit is expressed as a limit on the length of the miter.
- //
- // Defaults to 4.0. Using zero as a limit will cause a [StrokeJoin.bevel]
- // join to be used all the time.
+ /**
+ * The limit for miters to be drawn on segments when the join is set to
+ * [StrokeJoin.Miter] and the [style] is set to [PaintingStyle.Stroke]. If
+ * this limit is exceeded, then a [StrokeJoin.Bevel] join will be drawn
+ * instead. This may cause some 'popping' of the corners of a path if the
+ * angle between line segments is animated.
+ * This limit is expressed as a limit on the length of the miter.
+ * Defaults to 4.0. Using zero as a limit will cause a [StrokeJoin.Bevel]
+ * join to be used all the time.
+ */
var strokeMiterLimit: Float
- // Controls the performance vs quality trade-off to use when applying
- // when drawing images, as with [Canvas.drawImageRect]
- //
- // Defaults to [FilterQuality.none].
+ /**
+ * Controls the performance vs quality trade-off to use when applying
+ * when drawing images, as with [Canvas.drawImageRect]
+ * Defaults to [FilterQuality.None].
+ */
var filterQuality: FilterQuality
- // The shader to use when stroking or filling a shape.
- //
- // When this is null, the [color] is used instead.
- //
- // See also:
- //
- // * [Gradient], a shader that paints a color gradient.
- // * [ImageShader], a shader that tiles an [Image].
- // * [colorFilter], which overrides [shader].
- // * [color], which is used if [shader] and [colorFilter] are null.
+ /**
+ * The shader to use when stroking or filling a shape.
+ *
+ * When this is null, the [color] is used instead.
+ *
+ * See also:
+ * [LinearGradientShader], [RadialGradientShader], or [SweepGradientShader] shaders that
+ * paint a color gradient.
+ * [ImageShader], a shader that tiles an [ImageBitmap].
+ * [colorFilter], which overrides [shader].
+ * [color], which is used if [shader] and [colorFilter] are null.
+ */
var shader: Shader?
- // A color filter to apply when a shape is drawn or when a layer is
- // composited.
- //
- // See [ColorFilter] for details.
- //
- // When a shape is being drawn, [colorFilter] overrides [color] and [shader].
+ /**
+ * A color filter to apply when a shape is drawn or when a layer is
+ * composited.
+ * See [ColorFilter] for details.
+ * When a shape is being drawn, [colorFilter] overrides [color] and [shader].
+ */
var colorFilter: ColorFilter?
+ @Suppress("DEPRECATION")
+ @Deprecated(
+ "Use pathEffect instead",
+ ReplaceWith("pathEffect", "androidx.compose.ui.graphics.Paint")
+ )
var nativePathEffect: NativePathEffect?
+
+ /**
+ * Specifies the [PathEffect] applied to the geometry of the shape that is drawn
+ */
+ var pathEffect: PathEffect?
}
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt
index 91e53db..ccf90c0 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt
@@ -22,6 +22,10 @@
expect fun Path(): Path
+@Deprecated(
+ "Use PathEffect instead",
+ ReplaceWith("PathEffect", "androidx.compose.ui.graphics.PathEffect")
+)
expect class NativePathEffect
/* expect class */ interface Path {
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathEffect.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathEffect.kt
new file mode 100644
index 0000000..fb12a5d
--- /dev/null
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathEffect.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.graphics
+
+/**
+ * Effect applied to the geometry of a drawing primitive. For example, this can be used
+ * to draw shapes as a dashed or shaped pattern, or apply a treatment around line segment
+ * intersections.
+ */
+interface PathEffect {
+ companion object {
+
+ /**
+ * Replaces sharp angles between line segments into rounded angles of the specified radius
+ *
+ * @param radius Rounded corner radius to apply for each angle of the drawn shape
+ */
+ fun cornerPathEffect(radius: Float): PathEffect = actualCornerPathEffect(radius)
+
+ /**
+ * Draws a shape as a series of dashes with the given intervals and offset into the specified
+ * interval array. The intervals must contain an even number of entries (>=2). The even indices
+ * specify "on" intervals and the odd indices represent "off" intervals. The phase parameter
+ * is the pixel offset into the intervals array (mod the sum of all of the intervals).
+ *
+ * For example: if `intervals[] = {10, 20}`, and phase = 25, this will set up a dashed
+ * path like so: 5 pixels off 10 pixels on 20 pixels off 10 pixels on 20 pixels off
+ *
+ * The phase parameter is
+ * an offset into the intervals array. The intervals array
+ * controls the length of the dashes. This is only applied for stroked shapes
+ * (ex. [PaintingStyle.Stroke] and is ignored for filled in shapes (ex. [PaintingStyle.Fill]
+ *
+ * @param intervals Array of "on" and "off" distances for the dashed line segments
+ * @param phase Pixel offset into the intervals array
+ */
+ fun dashPathEffect(intervals: FloatArray, phase: Float = 0f): PathEffect =
+ actualDashPathEffect(intervals, phase)
+
+ /**
+ * Create a PathEffect that applies the inner effect to the path, and then applies the outer
+ * effect to the result of the inner effect. (e.g. outer(inner(path)).
+ */
+ fun chainPathEffect(outer: PathEffect, inner: PathEffect): PathEffect =
+ actualChainPathEffect(outer, inner)
+
+ /**
+ * Dash the drawn path by stamping it with the specified shape represented as a [Path].
+ * This is only applied to stroke shapes and will be ignored with filled shapes.
+ * The stroke width used with this [PathEffect] is ignored as well.
+ *
+ * @param shape Path to stamp along
+ * @param advance Spacing between each stamped shape
+ * @param phase Amount to offset before the first shape is stamped
+ * @param style How to transform the shape at each position as it is stamped
+ */
+ fun stampedPathEffect(
+ shape: Path,
+ advance: Float,
+ phase: Float,
+ style: StampedPathEffectStyle
+ ): PathEffect = actualStampedPathEffect(shape, advance, phase, style)
+ }
+}
+
+internal expect fun actualCornerPathEffect(radius: Float): PathEffect
+
+internal expect fun actualDashPathEffect(intervals: FloatArray, phase: Float): PathEffect
+
+internal expect fun actualChainPathEffect(outer: PathEffect, inner: PathEffect): PathEffect
+
+internal expect fun actualStampedPathEffect(
+ shape: Path,
+ advance: Float,
+ phase: Float,
+ style: StampedPathEffectStyle
+): PathEffect
+
+/**
+ * Strategy for transforming each point of the shape along the drawn path
+ *
+ * @sample androidx.compose.ui.graphics.samples.StampedPathEffectSample
+ */
+enum class StampedPathEffectStyle {
+
+ /**
+ * Translate the path shape into the specified location aligning the top left of the path with
+ * the drawn geometry. This does not modify the path itself.
+ *
+ * For example, a circle drawn with a square path and [Translate] will draw the square path
+ * repeatedly with the top left corner of each stamped square along the curvature of the circle.
+ */
+ Translate,
+
+ /**
+ * Rotates the path shape its center along the curvature of the drawn geometry. This does not
+ * modify the path itself.
+ *
+ * For example, a circle drawn with a square path and [Rotate] will draw the square path
+ * repeatedly with the center of each stamped square along the curvature of the circle as well
+ * as each square being rotated along the circumference.
+ */
+ Rotate,
+
+ /**
+ * Modifies the points within the path such that they fit within the drawn geometry. This will
+ * turn straight lines into curves.
+ *
+ * For example, a circle drawn with a square path and [Morph] will modify the straight lines
+ * of the square paths to be curves such that each stamped square is rendered as an arc around
+ * the curvature of the circle.
+ */
+ Morph
+}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/CanvasDrawScope.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/CanvasDrawScope.kt
index a73b4a4..ef1f005 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/CanvasDrawScope.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/CanvasDrawScope.kt
@@ -27,10 +27,10 @@
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.Matrix
-import androidx.compose.ui.graphics.NativePathEffect
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.PaintingStyle
import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
@@ -102,7 +102,7 @@
end: Offset,
strokeWidth: Float,
cap: StrokeCap,
- pathEffect: NativePathEffect?,
+ pathEffect: PathEffect?,
@FloatRange(from = 0.0, to = 1.0) alpha: Float,
colorFilter: ColorFilter?,
blendMode: BlendMode
@@ -131,7 +131,7 @@
end: Offset,
strokeWidth: Float,
cap: StrokeCap,
- pathEffect: NativePathEffect?,
+ pathEffect: PathEffect?,
@FloatRange(from = 0.0, to = 1.0) alpha: Float,
colorFilter: ColorFilter?,
blendMode: BlendMode
@@ -432,7 +432,7 @@
color: Color,
strokeWidth: Float,
cap: StrokeCap,
- pathEffect: NativePathEffect?,
+ pathEffect: PathEffect?,
@FloatRange(from = 0.0, to = 1.0) alpha: Float,
colorFilter: ColorFilter?,
blendMode: BlendMode
@@ -461,7 +461,7 @@
brush: Brush,
strokeWidth: Float,
cap: StrokeCap,
- pathEffect: NativePathEffect?,
+ pathEffect: PathEffect?,
@FloatRange(from = 0.0, to = 1.0) alpha: Float,
colorFilter: ColorFilter?,
blendMode: BlendMode
@@ -554,15 +554,11 @@
is Stroke ->
obtainStrokePaint()
.apply {
- with(drawStyle) {
- if (strokeWidth != width) strokeWidth = width
- if (strokeCap != cap) strokeCap = cap
- if (strokeMiterLimit != miter) strokeMiterLimit = miter
- if (strokeJoin != join) strokeJoin = join
-
- // TODO b/154550525 add PathEffect to Paint if necessary
- nativePathEffect = pathEffect
- }
+ if (strokeWidth != drawStyle.width) strokeWidth = drawStyle.width
+ if (strokeCap != drawStyle.cap) strokeCap = drawStyle.cap
+ if (strokeMiterLimit != drawStyle.miter) strokeMiterLimit = drawStyle.miter
+ if (strokeJoin != drawStyle.join) strokeJoin = drawStyle.join
+ if (pathEffect != drawStyle.pathEffect) pathEffect = drawStyle.pathEffect
}
}
@@ -612,7 +608,7 @@
miter: Float,
cap: StrokeCap,
join: StrokeJoin,
- pathEffect: NativePathEffect?,
+ pathEffect: PathEffect?,
@FloatRange(from = 0.0, to = 1.0) alpha: Float,
colorFilter: ColorFilter?,
blendMode: BlendMode
@@ -629,7 +625,7 @@
if (this.strokeMiterLimit != miter) this.strokeMiterLimit = miter
if (this.strokeCap != cap) this.strokeCap = cap
if (this.strokeJoin != join) this.strokeJoin = join
- this.nativePathEffect = pathEffect
+ if (this.pathEffect != pathEffect) this.pathEffect = pathEffect
}
private fun configureStrokePaint(
@@ -638,7 +634,7 @@
miter: Float,
cap: StrokeCap,
join: StrokeJoin,
- pathEffect: NativePathEffect?,
+ pathEffect: PathEffect?,
@FloatRange(from = 0.0, to = 1.0) alpha: Float,
colorFilter: ColorFilter?,
blendMode: BlendMode
@@ -654,7 +650,7 @@
if (this.strokeMiterLimit != miter) this.strokeMiterLimit = miter
if (this.strokeCap != cap) this.strokeCap = cap
if (this.strokeJoin != join) this.strokeJoin = join
- this.nativePathEffect = pathEffect
+ if (this.pathEffect != pathEffect) this.pathEffect = pathEffect
}
/**
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt
index 16f8b4f..26eeacd 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt
@@ -26,9 +26,9 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ImageBitmap
-import androidx.compose.ui.graphics.NativePathEffect
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
@@ -327,7 +327,7 @@
end: Offset,
strokeWidth: Float = Stroke.HairlineWidth,
cap: StrokeCap = Stroke.DefaultCap,
- pathEffect: NativePathEffect? = null,
+ pathEffect: PathEffect? = null,
@FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
colorFilter: ColorFilter? = null,
blendMode: BlendMode = DefaultBlendMode
@@ -354,7 +354,7 @@
end: Offset,
strokeWidth: Float = Stroke.HairlineWidth,
cap: StrokeCap = Stroke.DefaultCap,
- pathEffect: NativePathEffect? = null,
+ pathEffect: PathEffect? = null,
@FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
colorFilter: ColorFilter? = null,
blendMode: BlendMode = DefaultBlendMode
@@ -745,7 +745,7 @@
color: Color,
strokeWidth: Float = Stroke.HairlineWidth,
cap: StrokeCap = StrokeCap.Butt,
- pathEffect: NativePathEffect? = null,
+ pathEffect: PathEffect? = null,
@FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
colorFilter: ColorFilter? = null,
blendMode: BlendMode = DefaultBlendMode
@@ -773,7 +773,7 @@
brush: Brush,
strokeWidth: Float = Stroke.HairlineWidth,
cap: StrokeCap = StrokeCap.Butt,
- pathEffect: NativePathEffect? = null,
+ pathEffect: PathEffect? = null,
@FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
colorFilter: ColorFilter? = null,
blendMode: BlendMode = DefaultBlendMode
@@ -837,7 +837,7 @@
/**
* Effect to apply to the stroke, null indicates a solid stroke line is to be drawn
*/
- val pathEffect: NativePathEffect? = null
+ val pathEffect: PathEffect? = null
) : DrawStyle() {
companion object {
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPaint.kt b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPaint.kt
index a22418d..38cc267 100644
--- a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPaint.kt
+++ b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPaint.kt
@@ -114,9 +114,19 @@
field = value
}
- override var nativePathEffect: NativePathEffect? = null
+ override var nativePathEffect: NativePathEffect?
+ get() = pathEffect?.asDesktopPathEffect()
set(value) {
- skija.pathEffect = value
+ pathEffect = if (value == null) {
+ null
+ } else {
+ DesktopPathEffect(value)
+ }
+ }
+
+ override var pathEffect: PathEffect? = null
+ set(value) {
+ skija.pathEffect = (value as DesktopPathEffect).asDesktopPathEffect()
field = value
}
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPathEffect.kt b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPathEffect.kt
new file mode 100644
index 0000000..a443873
--- /dev/null
+++ b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPathEffect.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.graphics
+
+import org.jetbrains.skija.PathEffect as SkijaPathEffect
+
+internal class DesktopPathEffect(val nativePathEffect: SkijaPathEffect) : PathEffect
+
+/**
+ * Obtain a reference to the desktop PathEffect type
+ */
+fun PathEffect.asDesktopPathEffect(): SkijaPathEffect =
+ (this as DesktopPathEffect).nativePathEffect
+
+internal actual fun actualCornerPathEffect(radius: Float): PathEffect =
+ DesktopPathEffect(SkijaPathEffect.makeCorner(radius))
+
+internal actual fun actualDashPathEffect(
+ intervals: FloatArray,
+ phase: Float
+): PathEffect = DesktopPathEffect(SkijaPathEffect.makeDash(intervals, phase))
+
+internal actual fun actualChainPathEffect(outer: PathEffect, inner: PathEffect): PathEffect =
+ DesktopPathEffect(outer.asDesktopPathEffect().makeCompose(inner.asDesktopPathEffect()))
+
+internal actual fun actualStampedPathEffect(
+ shape: Path,
+ advance: Float,
+ phase: Float,
+ style: StampedPathEffectStyle
+): PathEffect =
+ DesktopPathEffect(
+ SkijaPathEffect.makePath1D(
+ shape.asDesktopPath(),
+ advance,
+ phase,
+ style.toSkijaStampedPathEffectStyle()
+ )
+ )
+
+internal fun StampedPathEffectStyle.toSkijaStampedPathEffectStyle(): SkijaPathEffect.Style =
+ when (this) {
+ StampedPathEffectStyle.Morph -> SkijaPathEffect.Style.MORPH
+ StampedPathEffectStyle.Rotate -> SkijaPathEffect.Style.ROTATE
+ StampedPathEffectStyle.Translate -> SkijaPathEffect.Style.TRANSLATE
+ }
\ No newline at end of file
diff --git a/compose/ui/ui-test-font/build.gradle b/compose/ui/ui-test-font/build.gradle
index 16d1675..995fd29 100644
--- a/compose/ui/ui-test-font/build.gradle
+++ b/compose/ui/ui-test-font/build.gradle
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+
+import androidx.build.AndroidXUiPlugin
import androidx.build.LibraryGroups
import androidx.build.LibraryVersions
import androidx.build.Publish
@@ -25,6 +27,19 @@
id("AndroidXUiPlugin")
}
+AndroidXUiPlugin.applyAndConfigureKotlinPlugin(project)
+
+if(AndroidXUiPlugin.isMultiplatformEnabled(project)) {
+ kotlin {
+ android()
+ jvm("desktop")
+
+ sourceSets {
+ desktopMain.dependsOn jvmMain
+ }
+ }
+}
+
androidx {
name = "Compose Test Font resources"
publish = Publish.NONE
diff --git a/compose/ui/ui-test-font/src/main/AndroidManifest.xml b/compose/ui/ui-test-font/src/androidMain/AndroidManifest.xml
similarity index 100%
rename from compose/ui/ui-test-font/src/main/AndroidManifest.xml
rename to compose/ui/ui-test-font/src/androidMain/AndroidManifest.xml
diff --git a/compose/ui/ui-test-font/src/main/res/font/invalid_font.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/invalid_font.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/invalid_font.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/invalid_font.ttf
diff --git a/compose/ui/ui-test-font/src/main/res/font/kern_font.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/kern_font.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/kern_font.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/kern_font.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/sample_font.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/sample_font.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/sample_font.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/sample_font.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/sample_font2.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/sample_font2.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/sample_font2.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/sample_font2.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_100_italic.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_100_italic.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_100_italic.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_100_italic.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_100_regular.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_100_regular.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_100_regular.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_100_regular.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_200_italic.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_200_italic.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_200_italic.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_200_italic.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_200_regular.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_200_regular.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_200_regular.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_200_regular.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_300_italic.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_300_italic.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_300_italic.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_300_italic.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_300_regular.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_300_regular.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_300_regular.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_300_regular.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_400_italic.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_400_italic.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_400_italic.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_400_italic.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_400_regular.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_400_regular.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_400_regular.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_400_regular.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_500_italic.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_500_italic.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_500_italic.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_500_italic.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_500_regular.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_500_regular.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_500_regular.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_500_regular.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_600_italic.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_600_italic.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_600_italic.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_600_italic.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_600_regular.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_600_regular.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_600_regular.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_600_regular.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_700_italic.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_700_italic.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_700_italic.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_700_italic.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_700_regular.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_700_regular.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_700_regular.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_700_regular.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_800_italic.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_800_italic.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_800_italic.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_800_italic.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_800_regular.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_800_regular.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_800_regular.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_800_regular.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_900_italic.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_900_italic.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_900_italic.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_900_italic.ttf
Binary files differ
diff --git a/compose/ui/ui-test-font/src/main/res/font/test_900_regular.ttf b/compose/ui/ui-test-font/src/commonMain/resources/font/test_900_regular.ttf
similarity index 100%
rename from compose/ui/ui-test-font/src/main/res/font/test_900_regular.ttf
rename to compose/ui/ui-test-font/src/commonMain/resources/font/test_900_regular.ttf
Binary files differ
diff --git a/compose/ui/ui-test-junit4/api/current.txt b/compose/ui/ui-test-junit4/api/current.txt
index e6bda1c2..f4d018a 100644
--- a/compose/ui/ui-test-junit4/api/current.txt
+++ b/compose/ui/ui-test-junit4/api/current.txt
@@ -7,7 +7,7 @@
public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeTestRule {
ctor public AndroidComposeTestRule(R activityRule, kotlin.jvm.functions.Function1<? super R,? extends A> activityProvider);
- method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
+ method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
method @androidx.compose.ui.test.ExperimentalTesting public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public R getActivityRule();
method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
@@ -15,9 +15,11 @@
method public long getDisplaySize-YbymL2g();
method public androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodes(androidx.compose.ui.test.SemanticsMatcher matcher, boolean useUnmergedTree);
method public androidx.compose.ui.test.SemanticsNodeInteraction onNode(androidx.compose.ui.test.SemanticsMatcher matcher, boolean useUnmergedTree);
+ method public void registerIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
method public <T> T! runOnIdle(kotlin.jvm.functions.Function0<? extends T> action);
method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+ method public void unregisterIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
method public void waitForIdle();
property public final R activityRule;
property public androidx.compose.ui.test.junit4.AnimationClockTestRule clockTestRule;
@@ -54,9 +56,11 @@
method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
method public androidx.compose.ui.unit.Density getDensity();
method public long getDisplaySize-YbymL2g();
+ method public void registerIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
method public <T> T! runOnIdle(kotlin.jvm.functions.Function0<? extends T> action);
method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+ method public void unregisterIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
method public void waitForIdle();
property public abstract androidx.compose.ui.test.junit4.AnimationClockTestRule clockTestRule;
property public abstract androidx.compose.ui.unit.Density density;
@@ -96,5 +100,8 @@
ctor public ComposeNotIdleException(String? message, Throwable? cause);
}
+ public final class EspressoLinkKt {
+ }
+
}
diff --git a/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt b/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
index e6bda1c2..f4d018a 100644
--- a/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
@@ -7,7 +7,7 @@
public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeTestRule {
ctor public AndroidComposeTestRule(R activityRule, kotlin.jvm.functions.Function1<? super R,? extends A> activityProvider);
- method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
+ method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
method @androidx.compose.ui.test.ExperimentalTesting public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public R getActivityRule();
method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
@@ -15,9 +15,11 @@
method public long getDisplaySize-YbymL2g();
method public androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodes(androidx.compose.ui.test.SemanticsMatcher matcher, boolean useUnmergedTree);
method public androidx.compose.ui.test.SemanticsNodeInteraction onNode(androidx.compose.ui.test.SemanticsMatcher matcher, boolean useUnmergedTree);
+ method public void registerIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
method public <T> T! runOnIdle(kotlin.jvm.functions.Function0<? extends T> action);
method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+ method public void unregisterIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
method public void waitForIdle();
property public final R activityRule;
property public androidx.compose.ui.test.junit4.AnimationClockTestRule clockTestRule;
@@ -54,9 +56,11 @@
method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
method public androidx.compose.ui.unit.Density getDensity();
method public long getDisplaySize-YbymL2g();
+ method public void registerIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
method public <T> T! runOnIdle(kotlin.jvm.functions.Function0<? extends T> action);
method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+ method public void unregisterIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
method public void waitForIdle();
property public abstract androidx.compose.ui.test.junit4.AnimationClockTestRule clockTestRule;
property public abstract androidx.compose.ui.unit.Density density;
@@ -96,5 +100,8 @@
ctor public ComposeNotIdleException(String? message, Throwable? cause);
}
+ public final class EspressoLinkKt {
+ }
+
}
diff --git a/compose/ui/ui-test-junit4/api/restricted_current.txt b/compose/ui/ui-test-junit4/api/restricted_current.txt
index e6bda1c2..f4d018a 100644
--- a/compose/ui/ui-test-junit4/api/restricted_current.txt
+++ b/compose/ui/ui-test-junit4/api/restricted_current.txt
@@ -7,7 +7,7 @@
public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeTestRule {
ctor public AndroidComposeTestRule(R activityRule, kotlin.jvm.functions.Function1<? super R,? extends A> activityProvider);
- method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description? description);
+ method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
method @androidx.compose.ui.test.ExperimentalTesting public suspend Object? awaitIdle(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public R getActivityRule();
method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
@@ -15,9 +15,11 @@
method public long getDisplaySize-YbymL2g();
method public androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodes(androidx.compose.ui.test.SemanticsMatcher matcher, boolean useUnmergedTree);
method public androidx.compose.ui.test.SemanticsNodeInteraction onNode(androidx.compose.ui.test.SemanticsMatcher matcher, boolean useUnmergedTree);
+ method public void registerIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
method public <T> T! runOnIdle(kotlin.jvm.functions.Function0<? extends T> action);
method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+ method public void unregisterIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
method public void waitForIdle();
property public final R activityRule;
property public androidx.compose.ui.test.junit4.AnimationClockTestRule clockTestRule;
@@ -54,9 +56,11 @@
method public androidx.compose.ui.test.junit4.AnimationClockTestRule getClockTestRule();
method public androidx.compose.ui.unit.Density getDensity();
method public long getDisplaySize-YbymL2g();
+ method public void registerIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
method public <T> T! runOnIdle(kotlin.jvm.functions.Function0<? extends T> action);
method public <T> T! runOnUiThread(kotlin.jvm.functions.Function0<? extends T> action);
method public void setContent(kotlin.jvm.functions.Function0<kotlin.Unit> composable);
+ method public void unregisterIdlingResource(androidx.compose.ui.test.IdlingResource idlingResource);
method public void waitForIdle();
property public abstract androidx.compose.ui.test.junit4.AnimationClockTestRule clockTestRule;
property public abstract androidx.compose.ui.unit.Density density;
@@ -96,5 +100,8 @@
ctor public ComposeNotIdleException(String? message, Throwable? cause);
}
+ public final class EspressoLinkKt {
+ }
+
}
diff --git a/compose/ui/ui-test-junit4/build.gradle b/compose/ui/ui-test-junit4/build.gradle
index 3c79cab..da98a1d 100644
--- a/compose/ui/ui-test-junit4/build.gradle
+++ b/compose/ui/ui-test-junit4/build.gradle
@@ -123,7 +123,6 @@
android {
tasks.withType(KotlinCompile).configureEach {
kotlinOptions {
- freeCompilerArgs += ["-XXLanguage:-NewInference"]
useIR = true
}
}
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/AndroidOwnerRegistryTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/AndroidOwnerRegistryTest.kt
index 2c038c3..f07b0a2 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/AndroidOwnerRegistryTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/AndroidOwnerRegistryTest.kt
@@ -20,7 +20,7 @@
import android.view.View
import android.view.ViewGroup
import androidx.activity.ComponentActivity
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.platform.ViewRootForTest
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.test.junit4.android.AndroidOwnerRegistry
import androidx.test.ext.junit.rules.ActivityScenarioRule
@@ -44,8 +44,8 @@
private val onRegistrationChangedListener =
object : AndroidOwnerRegistry.OnRegistrationChangedListener {
- val recordedChanges = mutableListOf<Pair<AndroidOwner, Boolean>>()
- override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
+ val recordedChanges = mutableListOf<Pair<ViewRootForTest, Boolean>>()
+ override fun onRegistrationChanged(owner: ViewRootForTest, registered: Boolean) {
recordedChanges.add(Pair(owner, registered))
}
}
@@ -67,7 +67,7 @@
activityRule.scenario.onActivity { activity ->
// set the composable content and find an owner
activity.setContent { }
- val owner = activity.findOwner()
+ val owner = activity.findRootForTest()
// Then it is registered
assertThat(androidOwnerRegistry.getUnfilteredOwners()).isEqualTo(setOf(owner))
@@ -84,7 +84,7 @@
activityRule.scenario.onActivity { activity ->
// set the composable content and find an owner
activity.setContent { }
- val owner = activity.findOwner()
+ val owner = activity.findRootForTest()
// And remove it from the hierarchy
activity.setContentView(View(activity))
@@ -107,7 +107,7 @@
activityRule.scenario.onActivity { activity ->
// set the composable content and find an owner
activity.setContent { }
- val owner = activity.findOwner()
+ val owner = activity.findRootForTest()
// When we tear down the registry
androidOwnerRegistry.tearDownRegistry()
@@ -126,16 +126,16 @@
}
}
-private fun Activity.findOwner(): AndroidOwner {
+private fun Activity.findRootForTest(): ViewRootForTest {
val viewGroup = findViewById<ViewGroup>(android.R.id.content)
- return requireNotNull(viewGroup.findOwner())
+ return requireNotNull(viewGroup.findRootForTest())
}
-private fun View.findOwner(): AndroidOwner? {
- if (this is AndroidOwner) return this
+private fun View.findRootForTest(): ViewRootForTest? {
+ if (this is ViewRootForTest) return this
if (this is ViewGroup) {
for (i in 0 until childCount) {
- val owner = getChildAt(i).findOwner()
+ val owner = getChildAt(i).findRootForTest()
if (owner != null) {
return owner
}
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
index 0adcd1b..c04b95a 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
@@ -114,7 +114,7 @@
}
/**
- * Detailed test to verify if [ComposeIdlingResource.isIdle] reports idleness correctly at
+ * Detailed test to verify if [ComposeIdlingResource.isIdleNow] reports idleness correctly at
* key moments during the animation kick-off process.
*/
@Test
@@ -138,28 +138,28 @@
val wasIdleAfterRecompose = rule.runOnIdle {
// Record idleness before kickoff of animation
- wasIdleBeforeKickOff = composeIdlingResource.isIdle()
+ wasIdleBeforeKickOff = composeIdlingResource.isIdleNow
// Kick off the animation
animationRunning = true
animationState.value = AnimationStates.To
// Record idleness after kickoff of animation, but before the snapshot is applied
- wasIdleBeforeApplySnapshot = composeIdlingResource.isIdle()
+ wasIdleBeforeApplySnapshot = composeIdlingResource.isIdleNow
// Apply the snapshot
@OptIn(ExperimentalComposeApi::class)
Snapshot.sendApplyNotifications()
// Record idleness after this snapshot is applied
- wasIdleAfterApplySnapshot = composeIdlingResource.isIdle()
+ wasIdleAfterApplySnapshot = composeIdlingResource.isIdleNow
// Record idleness after the first recomposition
@OptIn(ExperimentalCoroutinesApi::class)
scope.async(start = CoroutineStart.UNDISPATCHED) {
// Await a single recomposition
withFrameNanos {}
- composeIdlingResource.isIdle()
+ composeIdlingResource.isIdleNow
}
}.let {
runBlocking {
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistryTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistryTest.kt
new file mode 100644
index 0000000..cde1ff1
--- /dev/null
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistryTest.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.test.junit4
+
+import androidx.compose.ui.test.IdlingResource
+import androidx.compose.ui.test.InternalTestingApi
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScope
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/*
+ * This class *could* be moved to the test source set, but that makes it more likely to be
+ * skipped if only connectedCheck is run.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class IdlingResourceRegistryTest {
+
+ private var onIdleCalled = false
+ @OptIn(ExperimentalCoroutinesApi::class)
+ private val scope = TestCoroutineScope()
+ @OptIn(InternalTestingApi::class)
+ private val registry = IdlingResourceRegistry(scope).apply {
+ setOnIdleCallback { onIdleCalled = true }
+ }
+
+ @After
+ fun verifyRegistryStoppedPolling() {
+ scope.cleanupTestCoroutines()
+ }
+
+ @Test
+ @UiThreadTest
+ fun isIdleNow_0_IdlingResources() {
+ assertThat(registry.isIdleNow).isTrue()
+ assertNotPolling()
+ }
+
+ @Test
+ @UiThreadTest
+ fun isIdleNow_1_IdlingResource() {
+ val resource = TestIdlingResource(true)
+ registry.registerIdlingResource(resource)
+ assertThat(registry.isIdleNow).isTrue()
+
+ resource.isIdleNow = false
+ assertThat(registry.isIdleNow).isFalse()
+
+ assertThatPollingStartsAndEnds {
+ resource.isIdleNow = true
+ }
+
+ resource.isIdleNow = false
+ assertThat(registry.isIdleNow).isFalse()
+
+ assertThatPollingStartsAndEnds {
+ resource.isIdleNow = true
+ }
+ }
+
+ @Test
+ @UiThreadTest
+ fun isIdleNow_2_IdlingResources() {
+ val resource1 = TestIdlingResource(true)
+ registry.registerIdlingResource(resource1)
+ val resource2 = TestIdlingResource(true)
+ registry.registerIdlingResource(resource2)
+
+ resource1.isIdleNow = true
+ resource2.isIdleNow = true
+ assertThat(registry.isIdleNow).isTrue()
+
+ resource1.isIdleNow = false
+ resource2.isIdleNow = true
+ assertThat(registry.isIdleNow).isFalse()
+
+ resource1.isIdleNow = true
+ resource2.isIdleNow = false
+ assertThat(registry.isIdleNow).isFalse()
+
+ resource1.isIdleNow = false
+ resource2.isIdleNow = false
+ assertThat(registry.isIdleNow).isFalse()
+
+ assertThatPollingStartsAndEnds {
+ resource1.isIdleNow = true
+ resource2.isIdleNow = true
+ }
+ }
+
+ @Test
+ @UiThreadTest
+ fun isIdleNow_true_doesNotStartPolling() {
+ registry.registerIdlingResource(TestIdlingResource(true))
+ assertThat(registry.isIdleNow).isTrue()
+ assertNotPolling()
+ }
+
+ @Test
+ @UiThreadTest
+ fun isIdleNow_false_doesNotStartPolling() {
+ registry.registerIdlingResource(TestIdlingResource(false))
+ assertThat(registry.isIdleNow).isFalse()
+ assertNotPolling()
+ }
+
+ @Test
+ @UiThreadTest
+ fun isIdleOrStartPolling_emptyRegister_doesNotStartPolling() {
+ assertThat(registry.isIdleNow).isTrue()
+ assertThat(registry.isIdleOrEnsurePolling()).isTrue()
+ assertNotPolling()
+ }
+
+ @Test
+ @UiThreadTest
+ fun isIdleOrStartPolling_idleRegister_doesNotStartPolling() {
+ registry.registerIdlingResource(TestIdlingResource(true))
+ assertThat(registry.isIdleOrEnsurePolling()).isTrue()
+ assertNotPolling()
+ }
+
+ @Test
+ @UiThreadTest
+ fun isIdleOrStartPolling_busyRegister_doesStartPolling() {
+ val resource = TestIdlingResource(false)
+ registry.registerIdlingResource(resource)
+
+ assertThatPollingStartsAndEnds {
+ resource.isIdleNow = true
+ }
+ }
+
+ private fun assertThatPollingStartsAndEnds(makeIdle: () -> Unit) {
+ // Check that we're not polling already ..
+ assertThat(scope.advanceUntilIdle()).isEqualTo(0L)
+ // .. and that we're not idle
+ assertThat(registry.isIdleNow).isFalse()
+
+ // Start the polling
+ onIdleCalled = false
+ assertThat(registry.isIdleOrEnsurePolling()).isFalse()
+
+ // Make the registry idle
+ makeIdle.invoke()
+
+ // Verify that it has polled ..
+ assertThat(scope.advanceUntilIdle()).isGreaterThan(0L)
+ // .. the registry is now idle
+ assertThat(registry.isIdleNow).isTrue()
+ // .. and the onIdle callback was called
+ assertThat(onIdleCalled).isTrue()
+ }
+
+ private fun assertNotPolling() {
+ // Check that no poll job is running ..
+ assertThat(scope.advanceUntilIdle()).isEqualTo(0L)
+ scope.cleanupTestCoroutines()
+ // .. and that the onIdle callback was not called
+ assertThat(onIdleCalled).isFalse()
+ }
+
+ private class TestIdlingResource(initialIdleness: Boolean) : IdlingResource {
+ override var isIdleNow: Boolean = initialIdleness
+ }
+}
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/SynchronizationMethodsTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/SynchronizationMethodsTest.kt
index 2e199ea..1ac7476 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/SynchronizationMethodsTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/SynchronizationMethodsTest.kt
@@ -18,10 +18,8 @@
import androidx.activity.ComponentActivity
import androidx.compose.testutils.expectError
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.platform.ViewRootForTest
import androidx.compose.ui.test.onNodeWithTag
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleOwner
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
@@ -51,7 +49,7 @@
@Before
fun addMockResumedOwner() {
- androidOwnerRegistry.registerOwner(mockResumedAndroidOwner())
+ androidOwnerRegistry.registerOwner(mockResumedRootForTest())
}
@Test
@@ -119,21 +117,9 @@
}
}
- private fun mockResumedAndroidOwner(): AndroidOwner {
- val lifecycle = mock<Lifecycle>()
- doReturn(Lifecycle.State.RESUMED).whenever(lifecycle).currentState
-
- val lifecycleOwner = mock<LifecycleOwner>()
- doReturn(lifecycle).whenever(lifecycleOwner).lifecycle
-
- val viewTreeOwners = AndroidOwner.ViewTreeOwners(
- lifecycleOwner = lifecycleOwner,
- viewModelStoreOwner = mock(),
- savedStateRegistryOwner = mock()
- )
- val owner = mock<AndroidOwner>()
- doReturn(viewTreeOwners).whenever(owner).viewTreeOwners
-
+ private fun mockResumedRootForTest(): ViewRootForTest {
+ val owner = mock<ViewRootForTest>()
+ doReturn(true).whenever(owner).isLifecycleInResumedState
return owner
}
}
\ No newline at end of file
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt
index a6fd566..886546d 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt
@@ -87,7 +87,7 @@
animationState.value = AnimationStates.To
// Changes need to trickle down the animation system, so compose should be non-idle
- assertThat(composeIdlingResource.isIdle()).isFalse()
+ assertThat(composeIdlingResource.isIdleNow).isFalse()
}
// Await recomposition
@@ -101,7 +101,7 @@
// Advance first half of the animation (.5 sec)
rule.runOnIdle {
clockTestRule.advanceClock(halfDuration)
- assertThat(composeIdlingResource.isIdle()).isFalse()
+ assertThat(composeIdlingResource.isIdleNow).isFalse()
}
// Await next animation frame
@@ -115,7 +115,7 @@
// Advance second half of the animation (.5 sec)
rule.runOnIdle {
clockTestRule.advanceClock(halfDuration)
- assertThat(composeIdlingResource.isIdle()).isFalse()
+ assertThat(composeIdlingResource.isIdleNow).isFalse()
}
// Await next animation frame
@@ -150,7 +150,7 @@
animationState.value = AnimationStates.To
// Changes need to trickle down the animation system, so compose should be non-idle
- assertThat(composeIdlingResource.isIdle()).isFalse()
+ assertThat(composeIdlingResource.isIdleNow).isFalse()
}
// Perform a single recomposition by awaiting the same signal as the Recomposer
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt
index 7fcac78..572d8c2 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt
@@ -23,12 +23,14 @@
import androidx.compose.runtime.Recomposer
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.IdlingResource
import androidx.compose.ui.test.InternalTestingApi
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.SemanticsNodeInteractionCollection
import androidx.compose.ui.test.createTestContext
import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
+import androidx.compose.ui.test.junit4.android.EspressoLink
import androidx.compose.ui.text.InternalTextApi
import androidx.compose.ui.text.input.textInputServiceFactory
import androidx.compose.ui.unit.Density
@@ -138,8 +140,13 @@
activityProvider: (R) -> A
) : this(activityRule, activityProvider, false)
+ private val idlingResourceRegistry = IdlingResourceRegistry()
+ private val espressoLink = EspressoLink(idlingResourceRegistry)
+
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- internal val composeIdlingResource = ComposeIdlingResource()
+ internal val composeIdlingResource = ComposeIdlingResource().also {
+ registerIdlingResource(it)
+ }
@ExperimentalTesting
override val clockTestRule: AnimationClockTestRule =
@@ -174,11 +181,13 @@
}
}
- override fun apply(base: Statement, description: Description?): Statement {
+ override fun apply(base: Statement, description: Description): Statement {
@Suppress("NAME_SHADOWING")
@OptIn(ExperimentalTesting::class)
return RuleChain
.outerRule { base, _ -> composeIdlingResource.getStatementFor(base) }
+ .around { base, _ -> idlingResourceRegistry.getStatementFor(base) }
+ .around { base, _ -> espressoLink.getStatementFor(base) }
.around(clockTestRule)
.around { base, _ -> AndroidComposeStatement(base) }
.around(activityRule)
@@ -235,6 +244,14 @@
return runOnUiThread(action)
}
+ override fun registerIdlingResource(idlingResource: IdlingResource) {
+ idlingResourceRegistry.registerIdlingResource(idlingResource)
+ }
+
+ override fun unregisterIdlingResource(idlingResource: IdlingResource) {
+ idlingResourceRegistry.unregisterIdlingResource(idlingResource)
+ }
+
inner class AndroidComposeStatement(
private val base: Statement
) : Statement() {
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt
index b7ad00d..ded27d1 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt
@@ -18,7 +18,7 @@
import android.annotation.SuppressLint
import androidx.compose.ui.node.Owner
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.platform.ViewRootForTest
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.test.InternalTestingApi
import androidx.compose.ui.test.TestOwner
@@ -33,7 +33,7 @@
@SuppressLint("DocumentExceptions")
override fun sendTextInputCommand(node: SemanticsNode, command: List<EditOperation>) {
- val owner = node.layoutNode.owner as AndroidOwner
+ val owner = node.owner as ViewRootForTest
@Suppress("DEPRECATION")
runOnUiThread {
@@ -46,7 +46,7 @@
@SuppressLint("DocumentExceptions")
override fun sendImeAction(node: SemanticsNode, actionSpecified: ImeAction) {
- val owner = node.layoutNode.owner as AndroidOwner
+ val owner = node.owner as ViewRootForTest
@Suppress("DEPRECATION")
runOnUiThread {
@@ -66,7 +66,7 @@
return composeIdlingResource.getOwners()
}
- private fun AndroidOwner.getTextInputServiceOrDie(): TextInputServiceForTests {
+ private fun ViewRootForTest.getTextInputServiceOrDie(): TextInputServiceForTests {
return textInputService as? TextInputServiceForTests
?: throw IllegalStateException(
"Text input service wrapper not set up! Did you use ComposeTestRule?"
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt
index 0138a73..32fdccc 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt
@@ -18,9 +18,8 @@
import android.view.View
import androidx.annotation.VisibleForTesting
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.platform.ViewRootForTest
import androidx.compose.ui.test.ExperimentalTesting
-import androidx.lifecycle.Lifecycle
import kotlinx.coroutines.suspendCancellableCoroutine
import org.junit.runners.model.Statement
import java.util.Collections
@@ -32,24 +31,25 @@
import kotlin.time.ExperimentalTime
/**
- * Registry where all [AndroidOwner]s should be registered while they are attached to the window.
- * This registry is used by the testing library to query the owners's state.
+ * Registry where all views implementing [ViewRootForTest] should be registered while they
+ * are attached to the window. This registry is used by the testing library to query the owners's
+ * state.
*/
internal class AndroidOwnerRegistry {
- private val owners = Collections.newSetFromMap(WeakHashMap<AndroidOwner, Boolean>())
+ private val owners = Collections.newSetFromMap(WeakHashMap<ViewRootForTest, Boolean>())
private val registryListeners = mutableSetOf<OnRegistrationChangedListener>()
/**
- * Returns if the registry is setup to receive registrations from [AndroidOwner]s
+ * Returns if the registry is setup to receive registrations from [ViewRootForTest]s
*/
val isSetUp: Boolean
- get() = AndroidOwner.onAndroidOwnerCreatedCallback == ::onAndroidOwnerCreated
+ get() = ViewRootForTest.onViewCreatedCallback == ::onComposeViewCreated
/**
- * Sets up this registry to be notified of any [AndroidOwner] created
+ * Sets up this registry to be notified of any [ViewRootForTest] created
*/
private fun setupRegistry() {
- AndroidOwner.onAndroidOwnerCreatedCallback = ::onAndroidOwnerCreated
+ ViewRootForTest.onViewCreatedCallback = ::onComposeViewCreated
}
/**
@@ -57,9 +57,9 @@
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun tearDownRegistry() {
- AndroidOwner.onAndroidOwnerCreatedCallback = null
+ ViewRootForTest.onViewCreatedCallback = null
synchronized(owners) {
- getUnfilteredOwners().forEach {
+ owners.toList().forEach {
unregisterOwner(it)
}
// Remove all listeners as well, now we've unregistered all owners
@@ -67,33 +67,32 @@
}
}
- private fun onAndroidOwnerCreated(owner: AndroidOwner) {
+ private fun onComposeViewCreated(owner: ViewRootForTest) {
owner.view.addOnAttachStateChangeListener(OwnerAttachedListener(owner))
}
/**
- * Returns a copy of the set of all registered [AndroidOwner]s, including ones that are
+ * Returns a copy of the set of all registered [ViewRootForTest]s, including ones that are
* normally not relevant (like those whose lifecycle state is not RESUMED).
*/
- fun getUnfilteredOwners(): Set<AndroidOwner> {
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal fun getUnfilteredOwners(): Set<ViewRootForTest> {
return synchronized(owners) { owners.toSet() }
}
/**
- * Returns a copy of the set of all registered [AndroidOwner]s that can be interacted with.
- * This method is almost always preferred over [getUnfilteredOwners].
+ * Returns a copy of the set of all registered [ViewRootForTest]s that can be interacted with.
*/
- fun getOwners(): Set<AndroidOwner> {
+ fun getOwners(): Set<ViewRootForTest> {
return synchronized(owners) {
owners.filterTo(mutableSetOf()) {
- it.viewTreeOwners?.lifecycleOwner
- ?.lifecycle?.currentState == Lifecycle.State.RESUMED
+ it.isLifecycleInResumedState
}
}
}
/**
- * Adds the given [listener], to be notified when an [AndroidOwner] registers or unregisters.
+ * Adds the given [listener], to be notified when an [ViewRootForTest] registers or unregisters.
*/
fun addOnRegistrationChangedListener(listener: OnRegistrationChangedListener) {
synchronized(registryListeners) { registryListeners.add(listener) }
@@ -106,7 +105,7 @@
synchronized(registryListeners) { registryListeners.remove(listener) }
}
- private fun dispatchOnRegistrationChanged(owner: AndroidOwner, isRegistered: Boolean) {
+ private fun dispatchOnRegistrationChanged(owner: ViewRootForTest, isRegistered: Boolean) {
synchronized(registryListeners) { registryListeners.toList() }.forEach {
it.onRegistrationChanged(owner, isRegistered)
}
@@ -115,7 +114,7 @@
/**
* Registers the [owner] in this registry. Must be called from [View.onAttachedToWindow].
*/
- internal fun registerOwner(owner: AndroidOwner) {
+ internal fun registerOwner(owner: ViewRootForTest) {
synchronized(owners) {
if (owners.add(owner)) {
dispatchOnRegistrationChanged(owner, true)
@@ -126,7 +125,7 @@
/**
* Unregisters the [owner] from this registry. Must be called from [View.onDetachedFromWindow].
*/
- internal fun unregisterOwner(owner: AndroidOwner) {
+ internal fun unregisterOwner(owner: ViewRootForTest) {
synchronized(owners) {
if (owners.remove(owner)) {
dispatchOnRegistrationChanged(owner, false)
@@ -148,15 +147,15 @@
}
/**
- * Interface to be implemented by components that want to be notified when an [AndroidOwner]
+ * Interface to be implemented by components that want to be notified when an [ViewRootForTest]
* registers or unregisters at this registry.
*/
interface OnRegistrationChangedListener {
- fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean)
+ fun onRegistrationChanged(owner: ViewRootForTest, registered: Boolean)
}
private inner class OwnerAttachedListener(
- private val owner: AndroidOwner
+ private val owner: ViewRootForTest
) : View.OnAttachStateChangeListener {
// Note: owner.view === view, because the owner _is_ the view,
@@ -187,7 +186,7 @@
if (!hasAndroidOwners) {
val latch = CountDownLatch(1)
val listener = object : AndroidOwnerRegistry.OnRegistrationChangedListener {
- override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
+ override fun onRegistrationChanged(owner: ViewRootForTest, registered: Boolean) {
if (hasAndroidOwners) {
latch.countDown()
}
@@ -220,9 +219,12 @@
}
}
- // Usually we resume if an AndroidOwner is registered while the listener is added
+ // Usually we resume if an ComposeViewTestMarker is registered while the listener is added
val listener = object : AndroidOwnerRegistry.OnRegistrationChangedListener {
- override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
+ override fun onRegistrationChanged(
+ owner: ViewRootForTest,
+ registered: Boolean
+ ) {
if (hasAndroidOwners) {
resume(this)
}
@@ -234,7 +236,7 @@
removeOnRegistrationChangedListener(listener)
}
- // Sometimes the AndroidOwner was registered before we added
+ // Sometimes the ComposeViewTestMarker was registered before we added
// the listener, in which case we missed our signal
if (hasAndroidOwners) {
resume(listener)
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt
index ee8879a..d380d2f 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt
@@ -16,36 +16,19 @@
package androidx.compose.ui.test.junit4.android
-import android.os.Handler
-import android.os.Looper
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.node.Owner
import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.IdlingResource
import androidx.compose.ui.test.TestAnimationClock
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.junit4.isOnUiThread
-import androidx.compose.ui.test.junit4.runOnUiThread
-import androidx.test.espresso.AppNotIdleException
-import androidx.test.espresso.Espresso
-import androidx.test.espresso.IdlingRegistry
-import androidx.test.espresso.IdlingResource
-import androidx.test.espresso.IdlingResourceTimeoutException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.junit.runners.model.Statement
-import java.util.concurrent.atomic.AtomicInteger
-
-/**
- * In case Espresso times out, implementing this interface enables our resources to explain why
- * they failed to synchronize in case they were busy.
- */
-internal interface IdlingResourceWithDiagnostics {
- // TODO: Consider this as a public API.
- fun getDiagnosticMessageIfBusy(): String?
-}
/**
* Register compose's idling check to Espresso.
@@ -119,12 +102,7 @@
* resource is automatically registered when any compose testing APIs are used including
* [createAndroidComposeRule].
*/
-internal class ComposeIdlingResource : IdlingResource, IdlingResourceWithDiagnostics {
-
- override fun getName(): String = "ComposeIdlingResource"
-
- private var isIdleCheckScheduled = false
- private var resourceCallback: IdlingResource.ResourceCallback? = null
+internal class ComposeIdlingResource : IdlingResource {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal val androidOwnerRegistry = AndroidOwnerRegistry()
@@ -132,103 +110,34 @@
@OptIn(ExperimentalTesting::class)
private val clocks = mutableSetOf<TestAnimationClock>()
- private val handler = Handler(Looper.getMainLooper())
-
private var hadAnimationClocksIdle = true
private var hadNoSnapshotChanges = true
private var hadNoRecomposerChanges = true
- private var lastCompositionAwaiters = 0
private var hadNoPendingMeasureLayout = true
- // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
-// private var hadNoPendingDraw = true
-
- private var compositionAwaiters = AtomicInteger(0)
+ private var hadNoPendingDraw = true
/**
- * Returns whether or not Compose is idle, without starting to poll if it is not.
+ * Returns whether or not Compose is idle now.
*/
- @OptIn(ExperimentalComposeApi::class)
- fun isIdle(): Boolean {
- @Suppress("DEPRECATION")
- return runOnUiThread {
+ override val isIdleNow: Boolean
+ @OptIn(ExperimentalComposeApi::class)
+ get() {
hadNoSnapshotChanges = !Snapshot.current.hasPendingChanges()
hadNoRecomposerChanges = !Recomposer.current().hasInvalidations()
hadAnimationClocksIdle = areAllClocksIdle()
- lastCompositionAwaiters = compositionAwaiters.get()
- val owners = androidOwnerRegistry.getUnfilteredOwners()
+ val owners = androidOwnerRegistry.getOwners()
hadNoPendingMeasureLayout = !owners.any { it.hasPendingMeasureOrLayout }
- // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
-// hadNoPendingDraw = !owners.any {
-// val hasContent = it.view.measuredWidth != 0 && it.view.measuredHeight != 0
-// it.view.isDirty && (hasContent || it.view.isLayoutRequested)
-// }
-
- check(lastCompositionAwaiters >= 0) {
- "More CompositionAwaiters were removed then added ($lastCompositionAwaiters)"
+ hadNoPendingDraw = !owners.any {
+ val hasContent = it.view.measuredWidth != 0 && it.view.measuredHeight != 0
+ it.view.isDirty && (hasContent || it.view.isLayoutRequested)
}
- hadNoSnapshotChanges &&
+ return hadNoSnapshotChanges &&
hadNoRecomposerChanges &&
hadAnimationClocksIdle &&
- lastCompositionAwaiters == 0 &&
- // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
- hadNoPendingMeasureLayout /*&&
- hadNoPendingDraw*/
+ hadNoPendingMeasureLayout &&
+ hadNoPendingDraw
}
- }
-
- /**
- * Returns whether or not Compose is idle, and starts polling if it is not. Will always be
- * called from the main thread by Espresso, and should _only_ be called from Espresso. Use
- * [isIdle] if you need to query the idleness of Compose manually.
- */
- override fun isIdleNow(): Boolean {
- val isIdle = isIdle()
- if (!isIdle) {
- scheduleIdleCheck()
- }
- return isIdle
- }
-
- private fun scheduleIdleCheck() {
- if (!isIdleCheckScheduled) {
- isIdleCheckScheduled = true
- handler.postDelayed(
- {
- isIdleCheckScheduled = false
- if (isIdle()) {
- transitionToIdle()
- } else {
- scheduleIdleCheck()
- }
- }, /* delayMillis = */ 20
- )
- }
- }
-
- private fun transitionToIdle() {
- resourceCallback?.onTransitionToIdle()
- }
-
- override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
- resourceCallback = callback
- }
-
- /**
- * Called by [CompositionAwaiter] to indicate that this [ComposeIdlingResource] should report
- * busy to Espresso while that [CompositionAwaiter] is checking idleness.
- */
- internal fun addCompositionAwaiter() {
- compositionAwaiters.incrementAndGet()
- }
-
- /**
- * Called by [CompositionAwaiter] to indicate that this [ComposeIdlingResource] can report
- * idle as far as the calling [CompositionAwaiter] is concerned.
- */
- internal fun removeCompositionAwaiter() {
- compositionAwaiters.decrementAndGet()
- }
@OptIn(ExperimentalTesting::class)
fun registerTestClock(clock: TestAnimationClock) {
@@ -255,14 +164,10 @@
val hadSnapshotChanges = !hadNoSnapshotChanges
val hadRecomposerChanges = !hadNoRecomposerChanges
val hadRunningAnimations = !hadAnimationClocksIdle
- val numCompositionAwaiters = lastCompositionAwaiters
- val wasAwaitingCompositions = numCompositionAwaiters > 0
val hadPendingMeasureLayout = !hadNoPendingMeasureLayout
- // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
-// val hadPendingDraw = !hadNoPendingDraw
+ val hadPendingDraw = !hadNoPendingDraw
- val wasIdle = !hadSnapshotChanges && !hadRecomposerChanges &&
- !hadRunningAnimations && !wasAwaitingCompositions
+ val wasIdle = !hadSnapshotChanges && !hadRecomposerChanges && !hadRunningAnimations
if (wasIdle) {
return null
@@ -272,21 +177,19 @@
if (hadRunningAnimations) {
busyReasons.add("animations")
}
- val busyRecomposing = hadSnapshotChanges || hadRecomposerChanges || wasAwaitingCompositions
+ val busyRecomposing = hadSnapshotChanges || hadRecomposerChanges
if (busyRecomposing) {
busyReasons.add("pending recompositions")
}
- var message = "$name is busy due to ${busyReasons.joinToString(", ")}.\n"
+ var message = "${javaClass.simpleName} is busy due to ${busyReasons.joinToString(", ")}.\n"
if (busyRecomposing) {
message += "- Note: Timeout on pending recomposition means that there are most likely" +
" infinite re-compositions happening in the tested code.\n"
message += "- Debug: hadRecomposerChanges = $hadRecomposerChanges, "
message += "hadSnapshotChanges = $hadSnapshotChanges, "
- message += "numCompositionAwaiters = $numCompositionAwaiters, "
- message += "hadPendingMeasureLayout = $hadPendingMeasureLayout"
- // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
-// message += ", hadPendingDraw = $hadPendingDraw"
+ message += "hadPendingMeasureLayout = $hadPendingMeasureLayout, "
+ message += "hadPendingDraw = $hadPendingDraw"
}
return message
}
@@ -341,101 +244,6 @@
}
fun getStatementFor(base: Statement): Statement {
- return androidOwnerRegistry.getStatementFor(
- object : Statement() {
- override fun evaluate() {
- try {
- IdlingRegistry.getInstance().register(this@ComposeIdlingResource)
- base.evaluate()
- } finally {
- IdlingRegistry.getInstance().unregister(this@ComposeIdlingResource)
- }
- }
- }
- )
+ return androidOwnerRegistry.getStatementFor(base)
}
}
-
-// TODO(b/168223213): Make the CompositionAwaiter a suspend fun, remove ComposeIdlingResource
-// and blocking await Espresso.onIdle().
-internal fun ComposeIdlingResource.runEspressoOnIdle() {
- val compositionAwaiter = CompositionAwaiter(this)
- try {
- compositionAwaiter.start()
- Espresso.onIdle()
- } catch (e: Throwable) {
- compositionAwaiter.cancel()
-
- // Happens on the global time out, usually when global idling time out is less
- // or equal to dynamic idling time out or when the timeout is not due to individual
- // idling resource. This does not necessary mean that it can't be due to idling
- // resource being busy. So we try to check if it failed due to compose being busy and
- // add some extra information to the developer.
- val appNotIdleMaybe = tryToFindCause<AppNotIdleException>(e)
- if (appNotIdleMaybe != null) {
- rethrowWithMoreInfo(appNotIdleMaybe, wasGlobalTimeout = true)
- }
-
- // Happens on idling resource taking too long. Espresso gives out which resources caused
- // it but it won't allow us to give any extra information. So we check if it was our
- // resource and give more info if we can.
- val resourceNotIdleMaybe = tryToFindCause<IdlingResourceTimeoutException>(e)
- if (resourceNotIdleMaybe != null) {
- rethrowWithMoreInfo(resourceNotIdleMaybe, wasGlobalTimeout = false)
- }
-
- // No match, rethrow
- throw e
- }
-}
-
-private fun rethrowWithMoreInfo(e: Throwable, wasGlobalTimeout: Boolean) {
- var diagnosticInfo = ""
- val listOfIdlingResources = mutableListOf<String>()
- IdlingRegistry.getInstance().resources.forEach { resource ->
- if (resource is IdlingResourceWithDiagnostics) {
- val message = resource.getDiagnosticMessageIfBusy()
- if (message != null) {
- diagnosticInfo += "$message \n"
- }
- }
- listOfIdlingResources.add(resource.name)
- }
- if (diagnosticInfo.isNotEmpty()) {
- val prefix = if (wasGlobalTimeout) {
- "Global time out"
- } else {
- "Idling resource timed out"
- }
- throw ComposeNotIdleException(
- "$prefix: possibly due to compose being busy.\n" +
- diagnosticInfo +
- "All registered idling resources: " +
- listOfIdlingResources.joinToString(", "),
- e
- )
- }
- // No extra info, re-throw the original exception
- throw e
-}
-
-/**
- * Tries to find if the given exception or any of its cause is of the type of the provided
- * throwable T. Returns null if there is no match. This is required as some exceptions end up
- * wrapped in Runtime or Concurrent exceptions.
- */
-private inline fun <reified T : Throwable> tryToFindCause(e: Throwable): Throwable? {
- var causeToCheck: Throwable? = e
- while (causeToCheck != null) {
- if (causeToCheck is T) {
- return causeToCheck
- }
- causeToCheck = causeToCheck.cause
- }
- return null
-}
-
-/**
- * Thrown in cases where Compose can't get idle in Espresso's defined time limit.
- */
-class ComposeNotIdleException(message: String?, cause: Throwable?) : Throwable(message, cause)
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/CompositionAwaiter.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/CompositionAwaiter.kt
deleted file mode 100644
index dd380e9..0000000
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/CompositionAwaiter.kt
+++ /dev/null
@@ -1,124 +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.compose.ui.test.junit4.android
-
-import android.os.Handler
-import android.os.Looper
-import android.view.Choreographer
-import androidx.compose.runtime.ExperimentalComposeApi
-import androidx.compose.runtime.Recomposer
-import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.test.junit4.runOnUiThread
-
-internal class CompositionAwaiter(private val composeIdlingResource: ComposeIdlingResource) {
-
- private enum class State {
- Initialized, Running, Finished, Cancelled
- }
-
- private val lock = Any()
- private var state = State.Initialized
-
- private val handler = Handler(Looper.getMainLooper())
- @Suppress("DEPRECATION")
- private val choreographer = runOnUiThread { Choreographer.getInstance() }
-
- /**
- * Starts this awaiter, if it wasn't started, cancelled or finished yet.
- */
- fun start() {
- ifStateIsIn(State.Initialized) {
- state = State.Running
- startIdlingResource()
- handler.post(callback)
- }
- }
-
- /**
- * Cancels this awaiter if it is running or not yet started. Does nothing if it was already
- * finished or cancelled.
- */
- fun cancel() {
- ifStateIsIn(State.Initialized, State.Running) {
- state = State.Cancelled
- stopIdlingResource()
- handler.removeCallbacks(callback)
- choreographer.removeFrameCallback(callback)
- }
- }
-
- private fun startIdlingResource() {
- composeIdlingResource.addCompositionAwaiter()
- }
-
- private fun stopIdlingResource() {
- composeIdlingResource.removeCompositionAwaiter()
- }
-
- /**
- * Runs the given [block] if the current [state] is the [validStates]. Synchronizes on
- * [lock] to make it thread-safe.
- */
- private inline fun ifStateIsIn(vararg validStates: State, block: () -> Unit) {
- try {
- synchronized(lock) {
- if (state in validStates) {
- block()
- }
- }
- } catch (t: Throwable) {
- cancel()
- throw t
- }
- }
-
- @OptIn(ExperimentalComposeApi::class)
- private fun isIdle(): Boolean {
- return !Snapshot.current.hasPendingChanges() && !Recomposer.current().hasInvalidations()
- }
-
- private val callback = object : Runnable, Choreographer.FrameCallback {
- override fun run() {
- ifStateIsIn(State.Running) {
- if (!isIdle()) {
- // not idle, restart check. this makes sure our frame callback
- // will be executed _after_ potentially scheduled onCommits
- handler.postDelayed(this, 10)
- } else {
- // Is idle. Either nothing is scheduled on the choreographer, in which
- // case our callback will be the only one, or something is scheduled on
- // the choreographer, in which case our callback will be after it
- choreographer.postFrameCallback(this)
- }
- }
- }
-
- override fun doFrame(frameTime: Long) {
- ifStateIsIn(State.Running) {
- if (!isIdle()) {
- // not idle, restart check. onCommits have triggered a recomposition
- handler.postDelayed(this, 10)
- } else {
- // is idle. onCommits have _not_ triggered a
- // recomposition, or there were no onCommits
- state = State.Finished
- stopIdlingResource()
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/EspressoLink.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/EspressoLink.kt
new file mode 100644
index 0000000..631caf0
--- /dev/null
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/EspressoLink.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.test.junit4.android
+
+import androidx.compose.ui.test.junit4.IdlingResourceRegistry
+import androidx.test.espresso.AppNotIdleException
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.IdlingRegistry
+import androidx.test.espresso.IdlingResource
+import androidx.test.espresso.IdlingResourceTimeoutException
+import org.junit.runners.model.Statement
+
+internal class EspressoLink(private val registry: IdlingResourceRegistry) : IdlingResource {
+
+ override fun getName(): String = "Compose-Espresso link"
+
+ override fun isIdleNow(): Boolean {
+ return registry.isIdleOrEnsurePolling()
+ }
+
+ override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
+ registry.setOnIdleCallback {
+ callback?.onTransitionToIdle()
+ }
+ }
+
+ fun getDiagnosticMessageIfBusy(): String? = registry.getDiagnosticMessageIfBusy()
+
+ fun getStatementFor(base: Statement): Statement {
+ return object : Statement() {
+ override fun evaluate() {
+ try {
+ IdlingRegistry.getInstance().register(this@EspressoLink)
+ base.evaluate()
+ } finally {
+ IdlingRegistry.getInstance().unregister(this@EspressoLink)
+ }
+ }
+ }
+ }
+}
+
+// TODO(b/168223213): Make the CompositionAwaiter a suspend fun, remove ComposeIdlingResource
+// and blocking await Espresso.onIdle().
+internal fun runEspressoOnIdle() {
+ try {
+ Espresso.onIdle()
+ } catch (e: Throwable) {
+
+ // Happens on the global time out, usually when global idling time out is less
+ // or equal to dynamic idling time out or when the timeout is not due to individual
+ // idling resource. This does not necessary mean that it can't be due to idling
+ // resource being busy. So we try to check if it failed due to compose being busy and
+ // add some extra information to the developer.
+ val appNotIdleMaybe = tryToFindCause<AppNotIdleException>(e)
+ if (appNotIdleMaybe != null) {
+ rethrowWithMoreInfo(appNotIdleMaybe, wasGlobalTimeout = true)
+ }
+
+ // Happens on idling resource taking too long. Espresso gives out which resources caused
+ // it but it won't allow us to give any extra information. So we check if it was our
+ // resource and give more info if we can.
+ val resourceNotIdleMaybe = tryToFindCause<IdlingResourceTimeoutException>(e)
+ if (resourceNotIdleMaybe != null) {
+ rethrowWithMoreInfo(resourceNotIdleMaybe, wasGlobalTimeout = false)
+ }
+
+ // No match, rethrow
+ throw e
+ }
+}
+
+private fun rethrowWithMoreInfo(e: Throwable, wasGlobalTimeout: Boolean) {
+ var diagnosticInfo = ""
+ val listOfIdlingResources = mutableListOf<String>()
+ IdlingRegistry.getInstance().resources.forEach { resource ->
+ if (resource is EspressoLink) {
+ val message = resource.getDiagnosticMessageIfBusy()
+ if (message != null) {
+ diagnosticInfo += "$message \n"
+ }
+ }
+ listOfIdlingResources.add(resource.name)
+ }
+ if (diagnosticInfo.isNotEmpty()) {
+ val prefix = if (wasGlobalTimeout) {
+ "Global time out"
+ } else {
+ "Idling resource timed out"
+ }
+ throw ComposeNotIdleException(
+ "$prefix: possibly due to compose being busy.\n" +
+ diagnosticInfo +
+ "All registered idling resources: " +
+ listOfIdlingResources.joinToString(", "),
+ e
+ )
+ }
+ // No extra info, re-throw the original exception
+ throw e
+}
+
+/**
+ * Tries to find if the given exception or any of its cause is of the type of the provided
+ * throwable T. Returns null if there is no match. This is required as some exceptions end up
+ * wrapped in Runtime or Concurrent exceptions.
+ */
+private inline fun <reified T : Throwable> tryToFindCause(e: Throwable): Throwable? {
+ var causeToCheck: Throwable? = e
+ while (causeToCheck != null) {
+ if (causeToCheck is T) {
+ return causeToCheck
+ }
+ causeToCheck = causeToCheck.cause
+ }
+ return null
+}
+
+/**
+ * Thrown in cases where Compose can't get idle in Espresso's defined time limit.
+ */
+class ComposeNotIdleException(message: String?, cause: Throwable?) : Throwable(message, cause)
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt
index 282ac47..b694cc4 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt
@@ -26,6 +26,7 @@
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.IdlingResource
import androidx.compose.ui.test.InternalTestingApi
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteraction
@@ -130,6 +131,14 @@
return action().also { waitForIdle() }
}
+ override fun registerIdlingResource(idlingResource: IdlingResource) {
+ // TODO: implement
+ }
+
+ override fun unregisterIdlingResource(idlingResource: IdlingResource) {
+ // TODO: implement
+ }
+
override fun setContent(composable: @Composable () -> Unit) {
check(owner == null) {
"Cannot call setContent twice per test!"
diff --git a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.kt b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.kt
index 9fb0445..1190b77 100644
--- a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.kt
@@ -18,6 +18,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.IdlingResource
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
@@ -82,6 +83,16 @@
suspend fun awaitIdle()
/**
+ * Registers an [IdlingResource] in this test.
+ */
+ fun registerIdlingResource(idlingResource: IdlingResource)
+
+ /**
+ * Unregisters an [IdlingResource] from this test.
+ */
+ fun unregisterIdlingResource(idlingResource: IdlingResource)
+
+ /**
* Sets the given composable as a content of the current screen.
*
* Use this in your tests to setup the UI content to be tested. This should be called exactly
diff --git a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistry.kt b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistry.kt
new file mode 100644
index 0000000..5afcb3c
--- /dev/null
+++ b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/IdlingResourceRegistry.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.test.junit4
+
+import androidx.annotation.VisibleForTesting
+import androidx.compose.ui.test.IdlingResource
+import androidx.compose.ui.test.InternalTestingApi
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import org.junit.runners.model.Statement
+
+internal class IdlingResourceRegistry
+@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+@InternalTestingApi
+internal constructor(
+ private val pollScopeOverride: CoroutineScope?
+) : IdlingResource {
+ // Publicly facing constructor, that doesn't override the poll scope
+ @OptIn(InternalTestingApi::class)
+ constructor() : this(null)
+
+ private val lock = Any()
+
+ // All registered IdlingResources, both idle and busy ones
+ private val idlingResources = mutableSetOf<IdlingResource>()
+ // Each busy resource is mapped to the job that polls it
+ private val busyResources = mutableSetOf<IdlingResource>()
+ // The job that polls the resources until they are idle
+ private var pollJob: Job = Job().also { it.complete() }
+ // The scope in which to launch the poll job, or await the poll job
+ private val pollScope = pollScopeOverride ?: CoroutineScope(Dispatchers.Main)
+
+ private val isPolling: Boolean
+ get() = !pollJob.isCompleted
+
+ // Callback to be called every time when the last busy resource becomes idle
+ private var onIdle: (() -> Unit)? = null
+
+ /**
+ * Returns if all resources are idle
+ */
+ override val isIdleNow: Boolean get() {
+ @Suppress("DEPRECATION_ERROR")
+ return synchronized(lock) {
+ // If a poll job is running, we're not idle now. Let the job do its job.
+ !isPolling && areAllResourcesIdle()
+ }
+ }
+
+ /**
+ * Installs a callback that will be called when the registry transitions from busy to idle.
+ * Intended for the owner of the registry (e.g. AndroidComposeTestRule).
+ */
+ internal fun setOnIdleCallback(callback: () -> Unit) {
+ onIdle = callback
+ }
+
+ /**
+ * Registers the [idlingResource] into the registry
+ */
+ fun registerIdlingResource(idlingResource: IdlingResource) {
+ @Suppress("DEPRECATION_ERROR")
+ synchronized(lock) {
+ idlingResources.add(idlingResource)
+ }
+ }
+
+ /**
+ * Unregisters the [idlingResource] from the registry
+ */
+ fun unregisterIdlingResource(idlingResource: IdlingResource) {
+ @Suppress("DEPRECATION_ERROR")
+ synchronized(lock) {
+ idlingResources.remove(idlingResource)
+ busyResources.remove(idlingResource)
+ }
+ }
+
+ /**
+ * Starts polling the resources until all resources are idle. Won't start polling if all
+ * resources are already idle when this method is invoked, or if polling has already been
+ * started.
+ */
+ internal fun isIdleOrEnsurePolling(): Boolean {
+ @Suppress("DEPRECATION_ERROR")
+ return synchronized(lock) {
+ !isPolling && areAllResourcesIdle().also { isIdle ->
+ if (!isIdle) {
+ pollJob = pollScope.launch {
+ do {
+ delay(20)
+ } while (!areAllResourcesIdle())
+ onIdle?.invoke()
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks all resources for idleness, updates [busyResources] and returns if the registry is
+ * idle now.
+ */
+ private fun areAllResourcesIdle(): Boolean {
+ @Suppress("DEPRECATION_ERROR")
+ return synchronized(lock) {
+ busyResources.clear()
+ idlingResources.filterTo(busyResources) { !it.isIdleNow }.isEmpty()
+ }
+ }
+
+ override fun getDiagnosticMessageIfBusy(): String? {
+ val (idle, busy) =
+ @Suppress("DEPRECATION_ERROR")
+ synchronized(lock) {
+ if (busyResources.isEmpty()) {
+ return null
+ }
+ Pair(
+ (idlingResources - busyResources).toList(),
+ busyResources.map { it.getDiagnosticMessageIfBusy() ?: it.toString() }
+ )
+ }
+ return "IdlingResourceRegistry has the following idling resources registered:" +
+ busy.map { "\n- [busy] ${it.indentBy(" ")}" } +
+ idle.map { "\n- [idle] $it" } +
+ if (idle.isEmpty() && busy.isEmpty()) "\n<none>" else ""
+ }
+
+ /**
+ * Adds the given [prefix] after all non-terminal new lines.
+ *
+ * For example: `"\nfoo\nbar\n".indentBy("-")` gives `"\n-foo\n-bar\n"`
+ */
+ private fun String.indentBy(prefix: String): String {
+ return replace("\n(?=.)".toRegex(), "\n$prefix")
+ }
+
+ fun getStatementFor(base: Statement): Statement {
+ return object : Statement() {
+ override fun evaluate() {
+ try {
+ base.evaluate()
+ } finally {
+ if (pollScopeOverride == null) {
+ if (pollScope.coroutineContext[Job] != null) pollScope.cancel()
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/compose/ui/ui-test/api/current.txt b/compose/ui/ui-test/api/current.txt
index 8144343..6a6fa69 100644
--- a/compose/ui/ui-test/api/current.txt
+++ b/compose/ui/ui-test/api/current.txt
@@ -86,9 +86,9 @@
method public static androidx.compose.ui.test.SemanticsMatcher hasAnyDescendant(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasAnySibling(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasClickAction();
+ method public static androidx.compose.ui.test.SemanticsMatcher hasContentDescription(String label, optional boolean ignoreCase);
method public static androidx.compose.ui.test.SemanticsMatcher hasImeAction(androidx.compose.ui.text.input.ImeAction actionType);
method @Deprecated public static androidx.compose.ui.test.SemanticsMatcher hasInputMethodsSupport();
- method public static androidx.compose.ui.test.SemanticsMatcher hasLabel(String label, optional boolean ignoreCase);
method public static androidx.compose.ui.test.SemanticsMatcher hasNoClickAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasNoScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasParent(androidx.compose.ui.test.SemanticsMatcher matcher);
@@ -120,11 +120,11 @@
}
public final class FindersKt {
- method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithLabel(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
+ method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithContentDescription(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithSubstring(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithTag(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String testTag, optional boolean useUnmergedTree);
method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithText(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
- method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithLabel(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
+ method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithContentDescription(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithSubstring(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithTag(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String testTag, optional boolean useUnmergedTree);
method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithText(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
@@ -179,6 +179,12 @@
method public static void up(androidx.compose.ui.test.GestureScope, optional int pointerId);
}
+ public interface IdlingResource {
+ method public default String? getDiagnosticMessageIfBusy();
+ method public boolean isIdleNow();
+ property public abstract boolean isIdleNow;
+ }
+
@kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestingApi {
}
diff --git a/compose/ui/ui-test/api/public_plus_experimental_current.txt b/compose/ui/ui-test/api/public_plus_experimental_current.txt
index 8144343..6a6fa69 100644
--- a/compose/ui/ui-test/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test/api/public_plus_experimental_current.txt
@@ -86,9 +86,9 @@
method public static androidx.compose.ui.test.SemanticsMatcher hasAnyDescendant(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasAnySibling(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasClickAction();
+ method public static androidx.compose.ui.test.SemanticsMatcher hasContentDescription(String label, optional boolean ignoreCase);
method public static androidx.compose.ui.test.SemanticsMatcher hasImeAction(androidx.compose.ui.text.input.ImeAction actionType);
method @Deprecated public static androidx.compose.ui.test.SemanticsMatcher hasInputMethodsSupport();
- method public static androidx.compose.ui.test.SemanticsMatcher hasLabel(String label, optional boolean ignoreCase);
method public static androidx.compose.ui.test.SemanticsMatcher hasNoClickAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasNoScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasParent(androidx.compose.ui.test.SemanticsMatcher matcher);
@@ -120,11 +120,11 @@
}
public final class FindersKt {
- method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithLabel(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
+ method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithContentDescription(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithSubstring(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithTag(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String testTag, optional boolean useUnmergedTree);
method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithText(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
- method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithLabel(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
+ method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithContentDescription(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithSubstring(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithTag(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String testTag, optional boolean useUnmergedTree);
method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithText(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
@@ -179,6 +179,12 @@
method public static void up(androidx.compose.ui.test.GestureScope, optional int pointerId);
}
+ public interface IdlingResource {
+ method public default String? getDiagnosticMessageIfBusy();
+ method public boolean isIdleNow();
+ property public abstract boolean isIdleNow;
+ }
+
@kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestingApi {
}
diff --git a/compose/ui/ui-test/api/restricted_current.txt b/compose/ui/ui-test/api/restricted_current.txt
index 8144343..6a6fa69 100644
--- a/compose/ui/ui-test/api/restricted_current.txt
+++ b/compose/ui/ui-test/api/restricted_current.txt
@@ -86,9 +86,9 @@
method public static androidx.compose.ui.test.SemanticsMatcher hasAnyDescendant(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasAnySibling(androidx.compose.ui.test.SemanticsMatcher matcher);
method public static androidx.compose.ui.test.SemanticsMatcher hasClickAction();
+ method public static androidx.compose.ui.test.SemanticsMatcher hasContentDescription(String label, optional boolean ignoreCase);
method public static androidx.compose.ui.test.SemanticsMatcher hasImeAction(androidx.compose.ui.text.input.ImeAction actionType);
method @Deprecated public static androidx.compose.ui.test.SemanticsMatcher hasInputMethodsSupport();
- method public static androidx.compose.ui.test.SemanticsMatcher hasLabel(String label, optional boolean ignoreCase);
method public static androidx.compose.ui.test.SemanticsMatcher hasNoClickAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasNoScrollAction();
method public static androidx.compose.ui.test.SemanticsMatcher hasParent(androidx.compose.ui.test.SemanticsMatcher matcher);
@@ -120,11 +120,11 @@
}
public final class FindersKt {
- method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithLabel(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
+ method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithContentDescription(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithSubstring(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithTag(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String testTag, optional boolean useUnmergedTree);
method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection onAllNodesWithText(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
- method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithLabel(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
+ method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithContentDescription(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String label, optional boolean ignoreCase, optional boolean useUnmergedTree);
method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithSubstring(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithTag(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String testTag, optional boolean useUnmergedTree);
method public static androidx.compose.ui.test.SemanticsNodeInteraction onNodeWithText(androidx.compose.ui.test.SemanticsNodeInteractionsProvider, String text, optional boolean ignoreCase, optional boolean useUnmergedTree);
@@ -179,6 +179,12 @@
method public static void up(androidx.compose.ui.test.GestureScope, optional int pointerId);
}
+ public interface IdlingResource {
+ method public default String? getDiagnosticMessageIfBusy();
+ method public boolean isIdleNow();
+ property public abstract boolean isIdleNow;
+ }
+
@kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestingApi {
}
diff --git a/compose/ui/ui-test/build.gradle b/compose/ui/ui-test/build.gradle
index 7a19f17..3e55c2a 100644
--- a/compose/ui/ui-test/build.gradle
+++ b/compose/ui/ui-test/build.gradle
@@ -128,7 +128,6 @@
android {
tasks.withType(KotlinCompile).configureEach {
kotlinOptions {
- freeCompilerArgs += ["-XXLanguage:-NewInference"]
useIR = true
}
}
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/CallSemanticsActionTest.kt b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/CallSemanticsActionTest.kt
index 29ee38a..9319748 100644
--- a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/CallSemanticsActionTest.kt
+++ b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/CallSemanticsActionTest.kt
@@ -24,7 +24,7 @@
import androidx.compose.ui.semantics.AccessibilityAction
import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
-import androidx.compose.ui.semantics.accessibilityLabel
+import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.filters.MediumTest
@@ -46,18 +46,18 @@
val state = remember { mutableStateOf("Nothing") }
BoundaryNode {
setString("SetString") { state.value = it; return@setString true }
- accessibilityLabel = state.value
+ contentDescription = state.value
}
}
- rule.onNodeWithLabel("Nothing")
+ rule.onNodeWithContentDescription("Nothing")
.assertExists()
.performSemanticsAction(MyActions.SetString) { it("Hello") }
- rule.onNodeWithLabel("Nothing")
+ rule.onNodeWithContentDescription("Nothing")
.assertDoesNotExist()
- rule.onNodeWithLabel("Hello")
+ rule.onNodeWithContentDescription("Hello")
.assertExists()
}
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidAssertions.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidAssertions.kt
index 203fed5b..47c42c1 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidAssertions.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidAssertions.kt
@@ -19,8 +19,8 @@
import androidx.test.espresso.matcher.ViewMatchers
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.node.LayoutNode
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.layout.LayoutInfo
+import androidx.compose.ui.platform.ViewRootForTest
import androidx.compose.ui.semantics.SemanticsNode
internal actual fun SemanticsNodeInteraction.checkIsDisplayed(): Boolean {
@@ -28,16 +28,16 @@
val errorMessageOnFail = "Failed to perform isDisplayed check."
val node = fetchSemanticsNode(errorMessageOnFail)
- fun isNotPlaced(node: LayoutNode): Boolean {
+ fun isNotPlaced(node: LayoutInfo): Boolean {
return !node.isPlaced
}
- val layoutNode = node.layoutNode
- if (isNotPlaced(layoutNode) || layoutNode.findClosestParentNode(::isNotPlaced) != null) {
+ val layoutInfo = node.layoutInfo
+ if (isNotPlaced(layoutInfo) || layoutInfo.findClosestParentNode(::isNotPlaced) != null) {
return false
}
- (node.layoutNode.owner as? AndroidOwner)?.let {
+ (node.owner as? ViewRootForTest)?.let {
if (!ViewMatchers.isDisplayed().matches(it.view)) {
return false
}
@@ -53,7 +53,7 @@
}
internal actual fun SemanticsNode.clippedNodeBoundsInWindow(): Rect {
- val composeView = (layoutNode.owner as AndroidOwner).view
+ val composeView = (owner as ViewRootForTest).view
val rootLocationInWindow = intArrayOf(0, 0).let {
composeView.getLocationInWindow(it)
Offset(it[0].toFloat(), it[1].toFloat())
@@ -62,7 +62,7 @@
}
internal actual fun SemanticsNode.isInScreenBounds(): Boolean {
- val composeView = (layoutNode.owner as AndroidOwner).view
+ val composeView = (owner as ViewRootForTest).view
// Window relative bounds of our node
val nodeBoundsInWindow = clippedNodeBoundsInWindow()
@@ -83,17 +83,19 @@
}
/**
- * Executes [selector] on every parent of this [LayoutNode] and returns the closest
- * [LayoutNode] to return `true` from [selector] or null if [selector] returns false
+ * Executes [selector] on every parent of this [LayoutInfo] and returns the closest
+ * [LayoutInfo] to return `true` from [selector] or null if [selector] returns false
* for all ancestors.
*/
-private fun LayoutNode.findClosestParentNode(selector: (LayoutNode) -> Boolean): LayoutNode? {
- var currentParent = this.parent
+private fun LayoutInfo.findClosestParentNode(
+ selector: (LayoutInfo) -> Boolean
+): LayoutInfo? {
+ var currentParent = this.parentInfo
while (currentParent != null) {
if (selector(currentParent)) {
return currentParent
} else {
- currentParent = currentParent.parent
+ currentParent = currentParent.parentInfo
}
}
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidImageHelpers.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidImageHelpers.kt
index eaf81f0..e9de46c 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidImageHelpers.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidImageHelpers.kt
@@ -21,7 +21,7 @@
import android.view.Window
import androidx.annotation.RequiresApi
import androidx.compose.ui.graphics.ImageBitmap
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.platform.ViewRootForTest
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.android.captureRegionToImage
import androidx.compose.ui.window.DialogWindowProvider
@@ -53,7 +53,7 @@
)
}
- val view = (node.layoutNode.owner as AndroidOwner).view
+ val view = (node.owner as ViewRootForTest).view
// If we are in dialog use its window to capture the bitmap
val dialogParentNodeMaybe = node.findClosestParentNode(includeSelf = true) {
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.kt
index f94df4a..6dfc89b 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.kt
@@ -28,15 +28,15 @@
import androidx.compose.runtime.dispatch.AndroidUiDispatcher
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.node.Owner
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.platform.ViewRootForTest
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlin.math.max
internal actual fun createInputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher {
- require(owner is AndroidOwner) {
- "InputDispatcher currently only supports dispatching to AndroidOwner, not to " +
+ require(owner is ViewRootForTest) {
+ "InputDispatcher currently only supports dispatching to ViewRootForTest, not to " +
owner::class.java.simpleName
}
val view = owner.view
@@ -45,7 +45,7 @@
internal class AndroidInputDispatcher(
testContext: TestContext,
- owner: AndroidOwner?,
+ owner: Owner?,
private val sendEvent: (MotionEvent) -> Unit
) : InputDispatcher(testContext, owner) {
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
index de53bce..c680bb8 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
@@ -64,8 +64,8 @@
// Figure out the (clipped) bounds of the viewPort in its direct parent's content area, in
// root coordinates. We only want the clipping from the direct parent on the scrollable, not
// from any other ancestors.
- val viewPortInParent = scrollableNode.layoutNode.coordinates.boundsInParent
- val parentInRoot = scrollableNode.layoutNode.coordinates.parentCoordinates
+ val viewPortInParent = scrollableNode.layoutInfo.coordinates.boundsInParent
+ val parentInRoot = scrollableNode.layoutInfo.coordinates.parentCoordinates
?.positionInRoot ?: Offset.Zero
val viewPort = viewPortInParent.translate(parentInRoot)
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Assertions.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Assertions.kt
index d9ba5c5..9e06eed 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Assertions.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Assertions.kt
@@ -167,11 +167,11 @@
/**
* Asserts the node's label equals the given String.
- * For further details please check [SemanticsProperties.AccessibilityLabel].
+ * For further details please check [SemanticsProperties.ContentDescription].
* Throws [AssertionError] if the node's value is not equal to `value`, or if the node has no value
*/
fun SemanticsNodeInteraction.assertLabelEquals(value: String): SemanticsNodeInteraction =
- assert(hasLabel(value))
+ assert(hasContentDescription(value))
/**
* Asserts the node's text equals the given String.
@@ -184,7 +184,7 @@
/**
* Asserts the node's value equals the given value.
*
- * For further details please check [SemanticsProperties.AccessibilityValue].
+ * For further details please check [SemanticsProperties.StateDescription].
* Throws [AssertionError] if the node's value is not equal to `value`, or if the node has no value
*/
fun SemanticsNodeInteraction.assertValueEquals(value: String): SemanticsNodeInteraction =
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Filters.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Filters.kt
index 8626416..ac7ff43 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Filters.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Filters.kt
@@ -161,13 +161,13 @@
* @param label Text to match.
* @param ignoreCase Whether case should be ignored.
*
- * @see SemanticsProperties.AccessibilityLabel
+ * @see SemanticsProperties.ContentDescription
*/
-fun hasLabel(label: String, ignoreCase: Boolean = false): SemanticsMatcher {
+fun hasContentDescription(label: String, ignoreCase: Boolean = false): SemanticsMatcher {
return SemanticsMatcher(
- "${SemanticsProperties.AccessibilityLabel.name} = '$label' (ignoreCase: $ignoreCase)"
+ "${SemanticsProperties.ContentDescription.name} = '$label' (ignoreCase: $ignoreCase)"
) {
- it.config.getOrNull(SemanticsProperties.AccessibilityLabel).equals(label, ignoreCase)
+ it.config.getOrNull(SemanticsProperties.ContentDescription).equals(label, ignoreCase)
}
}
@@ -212,10 +212,10 @@
*
* @param value Value to match.
*
- * @see SemanticsProperties.AccessibilityValue
+ * @see SemanticsProperties.StateDescription
*/
fun hasValue(value: String): SemanticsMatcher = SemanticsMatcher.expectValue(
- SemanticsProperties.AccessibilityValue, value
+ SemanticsProperties.StateDescription, value
)
/**
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Finders.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Finders.kt
index 6d67243..2489cb4 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Finders.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Finders.kt
@@ -45,7 +45,7 @@
): SemanticsNodeInteractionCollection = onAllNodes(hasTestTag(testTag), useUnmergedTree)
/**
- * Finds a semantics node with the given label as its accessibilityLabel.
+ * Finds a semantics node with the given contentDescription.
*
* For usage patterns and semantics concepts see [SemanticsNodeInteraction]
*
@@ -53,11 +53,11 @@
*
* @see SemanticsNodeInteractionsProvider.onNode for general find method.
*/
-fun SemanticsNodeInteractionsProvider.onNodeWithLabel(
+fun SemanticsNodeInteractionsProvider.onNodeWithContentDescription(
label: String,
ignoreCase: Boolean = false,
useUnmergedTree: Boolean = false
-): SemanticsNodeInteraction = onNode(hasLabel(label, ignoreCase), useUnmergedTree)
+): SemanticsNodeInteraction = onNode(hasContentDescription(label, ignoreCase), useUnmergedTree)
/**
* Finds a semantincs node with the given text.
@@ -105,18 +105,19 @@
): SemanticsNodeInteractionCollection = onAllNodes(hasText(text, ignoreCase), useUnmergedTree)
/**
- * Finds all semantics nodes with the given label as AccessibilityLabel.
+ * Finds all semantics nodes with the given label as ContentDescription.
*
* For usage patterns and semantics concepts see [SemanticsNodeInteraction]
*
* @param useUnmergedTree Find within merged composables like Buttons.
* @see SemanticsNodeInteractionsProvider.onAllNodes for general find method.
*/
-fun SemanticsNodeInteractionsProvider.onAllNodesWithLabel(
+fun SemanticsNodeInteractionsProvider.onAllNodesWithContentDescription(
label: String,
ignoreCase: Boolean = false,
useUnmergedTree: Boolean = false
-): SemanticsNodeInteractionCollection = onAllNodes(hasLabel(label, ignoreCase), useUnmergedTree)
+): SemanticsNodeInteractionCollection =
+ onAllNodes(hasContentDescription(label, ignoreCase), useUnmergedTree)
/**
* Finds all semantics nodes with text that contains the given substring.
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/GestureScope.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/GestureScope.kt
index d4b7af9..c745d64 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/GestureScope.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/GestureScope.kt
@@ -106,7 +106,7 @@
}
// Convenience property
- private val owner get() = semanticsNode.layoutNode.owner
+ private val owner get() = semanticsNode.owner
// TODO(b/133217292): Better error: explain which gesture couldn't be performed
private var _inputDispatcher: InputDispatcher? =
@@ -285,7 +285,7 @@
* @param position A position in local coordinates
*/
private fun GestureScope.localToGlobal(position: Offset): Offset {
- return position + semanticsNode.layoutNode.coordinates.globalBounds.topLeft
+ return position + semanticsNode.layoutInfo.coordinates.globalBounds.topLeft
}
/**
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/IdlingResource.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/IdlingResource.kt
new file mode 100644
index 0000000..7abd443
--- /dev/null
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/IdlingResource.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.test
+
+/**
+ * Represents a resource of an application under test which can cause asynchronous background
+ * work to happen during test execution (e.g. an http request in response to a button click).
+ *
+ * By default, all interactions from the test with the compose tree (finding nodes, performing
+ * gestures, making assertions) will be synchronized with pending work in Compose's internals
+ * (such as applying state changes, recomposing, measuring, etc). This ensures that the UI is in
+ * a stable state when the interactions are performed, so that e.g. no pending recompositions are
+ * still scheduled that could potentially change the UI. However, any asynchronous work that is
+ * not done through one of Compose's mechanisms won't be included in the default synchronization.
+ * For such work, test authors can create an [IdlingResource] and register it into the test with
+ * [registerIdlingResource][androidx.compose.ui.test.junit4.ComposeTestRule
+ * .registerIdlingResource], and the interaction will wait for that resource to become idle prior
+ * to performing it.
+ */
+interface IdlingResource {
+ /**
+ * Whether or not the [IdlingResource] is idle when reading this value. This should always be
+ * called from the main thread, which is why it should be lightweight and fast.
+ *
+ * If one idling resource returns `false`, the synchronization system will keep polling all
+ * idling resources until they are all idle.
+ */
+ val isIdleNow: Boolean
+
+ /**
+ * Returns diagnostics that explain why the idling resource is busy, or `null` if the
+ * resource is not busy. Default implementation returns `null`.
+ */
+ fun getDiagnosticMessageIfBusy(): String? = null
+}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt
index 3892185..86871e9 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.test
-import androidx.compose.ui.input.key.ExperimentalKeyInput
import androidx.compose.ui.input.key.KeyEvent
/**
@@ -24,10 +23,9 @@
*
* @return true if the event was consumed. False otherwise.
*/
-@OptIn(ExperimentalKeyInput::class)
fun SemanticsNodeInteraction.performKeyPress(keyEvent: KeyEvent): Boolean {
val semanticsNode = fetchSemanticsNode("Failed to send key Event (${keyEvent.key})")
- val owner = semanticsNode.layoutNode.owner
+ val owner = semanticsNode.owner
requireNotNull(owner) { "Failed to find owner" }
@OptIn(InternalTestingApi::class)
return testContext.testOwner.runOnUiThread { owner.sendKeyEvent(keyEvent) }
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt
index 7b55269..74a2c6d 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt
@@ -248,7 +248,7 @@
operation: Density.(SemanticsNode) -> R
): R {
val node = fetchSemanticsNode("Failed to retrieve density for the node.")
- val density = node.layoutNode.owner!!.density
+ val density = node.owner!!.density
return operation.invoke(density, node)
}
@@ -256,7 +256,7 @@
assertion: Density.(Rect) -> Unit
): SemanticsNodeInteraction {
val node = fetchSemanticsNode("Failed to retrieve bounds of the node.")
- val density = node.layoutNode.owner!!.density
+ val density = node.owner!!.density
assertion.invoke(density, node.unclippedBoundsInRoot)
return this
diff --git a/compose/ui/ui-tooling/api/current.txt b/compose/ui/ui-tooling/api/current.txt
index 23de9c4..ca0c914 100644
--- a/compose/ui/ui-tooling/api/current.txt
+++ b/compose/ui/ui-tooling/api/current.txt
@@ -12,7 +12,7 @@
method public final java.util.Collection<java.lang.Object> getData();
method public final Object? getKey();
method public final androidx.compose.ui.tooling.SourceLocation? getLocation();
- method public java.util.List<androidx.compose.ui.node.ModifierInfo> getModifierInfo();
+ method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
method public final String? getName();
method public java.util.List<androidx.compose.ui.tooling.ParameterInformation> getParameters();
property public final androidx.compose.ui.unit.IntBounds box;
@@ -20,7 +20,7 @@
property public final java.util.Collection<java.lang.Object> data;
property public final Object? key;
property public final androidx.compose.ui.tooling.SourceLocation? location;
- property public java.util.List<androidx.compose.ui.node.ModifierInfo> modifierInfo;
+ property public java.util.List<androidx.compose.ui.layout.ModifierInfo> modifierInfo;
property public final String? name;
property public java.util.List<androidx.compose.ui.tooling.ParameterInformation> parameters;
}
@@ -41,9 +41,9 @@
}
public final class NodeGroup extends androidx.compose.ui.tooling.Group {
- ctor public NodeGroup(Object? key, Object node, androidx.compose.ui.unit.IntBounds box, java.util.Collection<?> data, java.util.List<androidx.compose.ui.node.ModifierInfo> modifierInfo, java.util.Collection<? extends androidx.compose.ui.tooling.Group> children);
+ ctor public NodeGroup(Object? key, Object node, androidx.compose.ui.unit.IntBounds box, java.util.Collection<?> data, java.util.List<androidx.compose.ui.layout.ModifierInfo> modifierInfo, java.util.Collection<? extends androidx.compose.ui.tooling.Group> children);
method public Object getNode();
- property public java.util.List<androidx.compose.ui.node.ModifierInfo> modifierInfo;
+ property public java.util.List<androidx.compose.ui.layout.ModifierInfo> modifierInfo;
property public final Object node;
}
diff --git a/compose/ui/ui-tooling/api/public_plus_experimental_current.txt b/compose/ui/ui-tooling/api/public_plus_experimental_current.txt
index 23de9c4..ca0c914 100644
--- a/compose/ui/ui-tooling/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-tooling/api/public_plus_experimental_current.txt
@@ -12,7 +12,7 @@
method public final java.util.Collection<java.lang.Object> getData();
method public final Object? getKey();
method public final androidx.compose.ui.tooling.SourceLocation? getLocation();
- method public java.util.List<androidx.compose.ui.node.ModifierInfo> getModifierInfo();
+ method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
method public final String? getName();
method public java.util.List<androidx.compose.ui.tooling.ParameterInformation> getParameters();
property public final androidx.compose.ui.unit.IntBounds box;
@@ -20,7 +20,7 @@
property public final java.util.Collection<java.lang.Object> data;
property public final Object? key;
property public final androidx.compose.ui.tooling.SourceLocation? location;
- property public java.util.List<androidx.compose.ui.node.ModifierInfo> modifierInfo;
+ property public java.util.List<androidx.compose.ui.layout.ModifierInfo> modifierInfo;
property public final String? name;
property public java.util.List<androidx.compose.ui.tooling.ParameterInformation> parameters;
}
@@ -41,9 +41,9 @@
}
public final class NodeGroup extends androidx.compose.ui.tooling.Group {
- ctor public NodeGroup(Object? key, Object node, androidx.compose.ui.unit.IntBounds box, java.util.Collection<?> data, java.util.List<androidx.compose.ui.node.ModifierInfo> modifierInfo, java.util.Collection<? extends androidx.compose.ui.tooling.Group> children);
+ ctor public NodeGroup(Object? key, Object node, androidx.compose.ui.unit.IntBounds box, java.util.Collection<?> data, java.util.List<androidx.compose.ui.layout.ModifierInfo> modifierInfo, java.util.Collection<? extends androidx.compose.ui.tooling.Group> children);
method public Object getNode();
- property public java.util.List<androidx.compose.ui.node.ModifierInfo> modifierInfo;
+ property public java.util.List<androidx.compose.ui.layout.ModifierInfo> modifierInfo;
property public final Object node;
}
diff --git a/compose/ui/ui-tooling/api/restricted_current.txt b/compose/ui/ui-tooling/api/restricted_current.txt
index 23de9c4..ca0c914 100644
--- a/compose/ui/ui-tooling/api/restricted_current.txt
+++ b/compose/ui/ui-tooling/api/restricted_current.txt
@@ -12,7 +12,7 @@
method public final java.util.Collection<java.lang.Object> getData();
method public final Object? getKey();
method public final androidx.compose.ui.tooling.SourceLocation? getLocation();
- method public java.util.List<androidx.compose.ui.node.ModifierInfo> getModifierInfo();
+ method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
method public final String? getName();
method public java.util.List<androidx.compose.ui.tooling.ParameterInformation> getParameters();
property public final androidx.compose.ui.unit.IntBounds box;
@@ -20,7 +20,7 @@
property public final java.util.Collection<java.lang.Object> data;
property public final Object? key;
property public final androidx.compose.ui.tooling.SourceLocation? location;
- property public java.util.List<androidx.compose.ui.node.ModifierInfo> modifierInfo;
+ property public java.util.List<androidx.compose.ui.layout.ModifierInfo> modifierInfo;
property public final String? name;
property public java.util.List<androidx.compose.ui.tooling.ParameterInformation> parameters;
}
@@ -41,9 +41,9 @@
}
public final class NodeGroup extends androidx.compose.ui.tooling.Group {
- ctor public NodeGroup(Object? key, Object node, androidx.compose.ui.unit.IntBounds box, java.util.Collection<?> data, java.util.List<androidx.compose.ui.node.ModifierInfo> modifierInfo, java.util.Collection<? extends androidx.compose.ui.tooling.Group> children);
+ ctor public NodeGroup(Object? key, Object node, androidx.compose.ui.unit.IntBounds box, java.util.Collection<?> data, java.util.List<androidx.compose.ui.layout.ModifierInfo> modifierInfo, java.util.Collection<? extends androidx.compose.ui.tooling.Group> children);
method public Object getNode();
- property public java.util.List<androidx.compose.ui.node.ModifierInfo> modifierInfo;
+ property public java.util.List<androidx.compose.ui.layout.ModifierInfo> modifierInfo;
property public final Object node;
}
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/ToolingTest.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/ToolingTest.kt
index bf3b298..d3d094f 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/ToolingTest.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/ToolingTest.kt
@@ -26,7 +26,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.R
import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.platform.ViewRootForTest
import androidx.compose.ui.platform.setContent
import org.junit.Before
import org.junit.Rule
@@ -81,9 +81,9 @@
WeakHashMap<SlotTable, Boolean>()
)
activityTestRule.onUiThread {
- AndroidOwner.onAndroidOwnerCreatedCallback = {
+ ViewRootForTest.onViewCreatedCallback = {
it.view.setTag(R.id.inspection_slot_table_set, map)
- AndroidOwner.onAndroidOwnerCreatedCallback = null
+ ViewRootForTest.onViewCreatedCallback = null
}
activity.setContent {
Box(
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt
index 73d6452..4ecf4eb 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt
@@ -22,6 +22,7 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.preferredHeight
+import androidx.compose.foundation.text.BasicText
import androidx.compose.material.Button
import androidx.compose.material.ModalDrawerLayout
import androidx.compose.material.Surface
@@ -33,6 +34,8 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.node.OwnedLayer
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.Group
import androidx.compose.ui.tooling.Inspectable
import androidx.compose.ui.tooling.R
@@ -461,6 +464,30 @@
assertThat(node).isNotNull()
}
+ @Test // regression test b/174855322
+ fun testBasicText() {
+ val slotTableRecord = SlotTableRecord.create()
+
+ view.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
+ show {
+ Column {
+ BasicText(
+ text = "Some text",
+ style = TextStyle(textDecoration = TextDecoration.Underline)
+ )
+ }
+ }
+
+ val builder = LayoutInspectorTree()
+ val node = builder.convert(view)
+ .flatMap { flatten(it) }
+ .firstOrNull { it.name == "BasicText" }
+
+ assertThat(node).isNotNull()
+
+ assertThat(node?.parameters).isNotEmpty()
+ }
+
@SdkSuppress(minSdkVersion = 29) // Render id is not returned for api < 29: b/171519437
@Test
@Ignore("b/174152464")
diff --git a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/Inspectable.kt b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/Inspectable.kt
index d8145bc..1f1b9d2 100644
--- a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/Inspectable.kt
+++ b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/Inspectable.kt
@@ -59,6 +59,7 @@
content: @Composable () -> Unit
) {
currentComposer.collectKeySourceInformation()
+ currentComposer.collectParameterInformation()
val store = (slotTableRecord as SlotTableRecordImpl).store
store.add(currentComposer.slotTable)
Providers(
diff --git a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/SlotTree.kt b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/SlotTree.kt
index e93ccb9..265bcfc 100644
--- a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/SlotTree.kt
+++ b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/SlotTree.kt
@@ -23,8 +23,8 @@
import androidx.compose.runtime.SlotTable
import androidx.compose.runtime.keySourceInfoOf
import androidx.compose.ui.layout.globalPosition
-import androidx.compose.ui.node.LayoutNode
-import androidx.compose.ui.node.ModifierInfo
+import androidx.compose.ui.layout.LayoutInfo
+import androidx.compose.ui.layout.ModifierInfo
import androidx.compose.ui.unit.IntBounds
import java.lang.reflect.Field
import kotlin.math.max
@@ -455,7 +455,7 @@
children.add(getGroup(context))
}
- val modifierInfo = if (node is LayoutNode) {
+ val modifierInfo = if (node is LayoutInfo) {
node.getModifierInfo()
} else {
emptyList()
@@ -463,7 +463,7 @@
// Calculate bounding box
val box = when (node) {
- is LayoutNode -> boundsOfLayoutNode(node)
+ is LayoutInfo -> boundsOfLayoutNode(node)
else ->
if (children.isEmpty()) emptyBox else
children.map { g -> g.box }.reduce { acc, box -> box.union(acc) }
@@ -491,8 +491,8 @@
)
}
-private fun boundsOfLayoutNode(node: LayoutNode): IntBounds {
- if (node.owner == null) {
+private fun boundsOfLayoutNode(node: LayoutInfo): IntBounds {
+ if (!node.isAttached) {
return IntBounds(
left = 0,
top = 0,
diff --git a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/InspectorNode.kt b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/InspectorNode.kt
index 5f5bcaf..c6add12 100644
--- a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/InspectorNode.kt
+++ b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/InspectorNode.kt
@@ -16,7 +16,7 @@
package androidx.compose.ui.tooling.inspector
-import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.layout.LayoutInfo
/**
* Node representing a Composable for the Layout Inspector.
@@ -106,7 +106,7 @@
*/
internal class MutableInspectorNode {
var id = 0L
- var layoutNodes = mutableListOf<LayoutNode>()
+ var layoutNodes = mutableListOf<LayoutInfo>()
var name = ""
var fileName = ""
var packageHash = -1
diff --git a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTree.kt b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTree.kt
index 6f30139..b831653 100644
--- a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTree.kt
+++ b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTree.kt
@@ -19,7 +19,7 @@
import android.view.View
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.SlotTable
-import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.layout.LayoutInfo
import androidx.compose.ui.node.OwnedLayer
import androidx.compose.ui.tooling.Group
import androidx.compose.ui.tooling.NodeGroup
@@ -62,8 +62,8 @@
private val parameterFactory = ParameterFactory(inlineClassConverter)
private val cache = ArrayDeque<MutableInspectorNode>()
private var generatedId = -1L
- /** Map from [LayoutNode] to the nearest [InspectorNode] that contains it */
- private val claimedNodes = IdentityHashMap<LayoutNode, InspectorNode>()
+ /** Map from [LayoutInfo] to the nearest [InspectorNode] that contains it */
+ private val claimedNodes = IdentityHashMap<LayoutInfo, InspectorNode>()
/** Map from parent tree to child trees that are about to be stitched together */
private val treeMap = IdentityHashMap<MutableInspectorNode, MutableList<MutableInspectorNode>>()
/** Map from owner node to child trees that are about to be stitched to this owner */
@@ -121,20 +121,20 @@
}
/**
- * Stitch separate trees together using the [LayoutNode]s found in the [SlotTable]s.
+ * Stitch separate trees together using the [LayoutInfo]s found in the [SlotTable]s.
*
* Some constructs in Compose (e.g. ModalDrawerLayout) will result is multiple [SlotTable]s.
* This code will attempt to stitch the resulting [InspectorNode] trees together by looking
- * at the parent of each [LayoutNode].
+ * at the parent of each [LayoutInfo].
* If this algorithm is successful the result of this function will be a list with a single
* tree.
*/
private fun stitchTreesByLayoutNode(trees: List<MutableInspectorNode>): List<InspectorNode> {
- val layoutToTreeMap = IdentityHashMap<LayoutNode, MutableInspectorNode>()
+ val layoutToTreeMap = IdentityHashMap<LayoutInfo, MutableInspectorNode>()
trees.forEach { tree -> tree.layoutNodes.forEach { layoutToTreeMap[it] = tree } }
trees.forEach { tree ->
val layout = tree.layoutNodes.lastOrNull()
- val parentLayout = generateSequence(layout) { it.parent }.firstOrNull {
+ val parentLayout = generateSequence(layout) { it.parentInfo }.firstOrNull {
val otherTree = layoutToTreeMap[it]
otherTree != null && otherTree != tree
}
@@ -262,7 +262,7 @@
private fun parse(group: Group): MutableInspectorNode {
val node = newNode()
node.id = getRenderNode(group)
- ((group as? NodeGroup)?.node as? LayoutNode)?.let { node.layoutNodes.add(it) }
+ ((group as? NodeGroup)?.node as? LayoutInfo)?.let { node.layoutNodes.add(it) }
if (!parseCallLocation(group, node) && group.name.isNullOrEmpty()) {
return markUnwanted(node)
}
diff --git a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/preview/ComposeViewAdapter.kt b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/preview/ComposeViewAdapter.kt
index 7e6d2b6..e25b4ae 100644
--- a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/preview/ComposeViewAdapter.kt
+++ b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/preview/ComposeViewAdapter.kt
@@ -40,9 +40,9 @@
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.AmbientAnimationClock
import androidx.compose.ui.platform.AmbientFontLoader
-import androidx.compose.ui.platform.AndroidOwner
import androidx.compose.ui.platform.AnimationClockAmbient
import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewRootForTest
import androidx.compose.ui.tooling.Group
import androidx.compose.ui.tooling.Inspectable
import androidx.compose.ui.tooling.SlotTableRecord
@@ -461,7 +461,8 @@
// (an AndroidOwner) when setting the clock time to make sure the Compose
// Preview will animate when the states are read inside the draw scope.
val composeView = getChildAt(0) as ComposeView
- (composeView.getChildAt(0) as? AndroidOwner)?.invalidateDescendants()
+ (composeView.getChildAt(0) as? ViewRootForTest)
+ ?.invalidateDescendants()
}
Providers(AmbientAnimationClock provides clock) {
composable()
diff --git a/compose/ui/ui-util/api/current.txt b/compose/ui/ui-util/api/current.txt
index 8f93c6c..efa8bdd 100644
--- a/compose/ui/ui-util/api/current.txt
+++ b/compose/ui/ui-util/api/current.txt
@@ -34,10 +34,6 @@
method public static Object nativeClass(Object);
}
- public final class JvmSynchronizationHelperKt {
- method public static <T> T! synchronized(Object lock, kotlin.jvm.functions.Function0<? extends T> block);
- }
-
public final class ListUtilsKt {
method public static inline <T> boolean fastAll(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
method public static inline <T> boolean fastAny(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
diff --git a/compose/ui/ui-util/api/public_plus_experimental_current.txt b/compose/ui/ui-util/api/public_plus_experimental_current.txt
index 8f93c6c..efa8bdd 100644
--- a/compose/ui/ui-util/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-util/api/public_plus_experimental_current.txt
@@ -34,10 +34,6 @@
method public static Object nativeClass(Object);
}
- public final class JvmSynchronizationHelperKt {
- method public static <T> T! synchronized(Object lock, kotlin.jvm.functions.Function0<? extends T> block);
- }
-
public final class ListUtilsKt {
method public static inline <T> boolean fastAll(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
method public static inline <T> boolean fastAny(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
diff --git a/compose/ui/ui-util/api/restricted_current.txt b/compose/ui/ui-util/api/restricted_current.txt
index 8f93c6c..efa8bdd 100644
--- a/compose/ui/ui-util/api/restricted_current.txt
+++ b/compose/ui/ui-util/api/restricted_current.txt
@@ -34,10 +34,6 @@
method public static Object nativeClass(Object);
}
- public final class JvmSynchronizationHelperKt {
- method public static <T> T! synchronized(Object lock, kotlin.jvm.functions.Function0<? extends T> block);
- }
-
public final class ListUtilsKt {
method public static inline <T> boolean fastAll(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
method public static inline <T> boolean fastAny(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
diff --git a/compose/ui/ui-util/src/jvmMain/kotlin/androidx/compose/ui/util/JvmSynchronizationHelper.kt b/compose/ui/ui-util/src/jvmMain/kotlin/androidx/compose/ui/util/JvmSynchronizationHelper.kt
deleted file mode 100644
index f94d1ec..0000000
--- a/compose/ui/ui-util/src/jvmMain/kotlin/androidx/compose/ui/util/JvmSynchronizationHelper.kt
+++ /dev/null
@@ -1,34 +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.compose.ui.util
-
-import kotlin.contracts.ExperimentalContracts
-import kotlin.contracts.InvocationKind
-import kotlin.contracts.contract
-
-/**
- * [kotlin.synchronized][synchronized] is deprecated, and the build fails if we use
- * [kotlin.synchronized][synchronized] along with the IR compiler. As a workaround, we have this
- * function here, which is in a module that doesn't use the IR Compiler.
- */
-@OptIn(ExperimentalContracts::class)
-fun <T> synchronized(lock: Any, block: () -> T): T {
- contract {
- callsInPlace(block, InvocationKind.EXACTLY_ONCE)
- }
- return kotlin.synchronized(lock, block)
-}
\ No newline at end of file
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index c05fc73..9f00a14c 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -132,26 +132,29 @@
method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
}
- public final class FocusModifierKt {
- method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focus(androidx.compose.ui.Modifier);
+ @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalComposeUiApi {
}
- @androidx.compose.ui.focus.ExperimentalFocus public interface FocusObserverModifier extends androidx.compose.ui.Modifier.Element {
+ public final class FocusModifierKt {
+ method public static androidx.compose.ui.Modifier focus(androidx.compose.ui.Modifier);
+ }
+
+ public interface FocusObserverModifier extends androidx.compose.ui.Modifier.Element {
method public kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusState,kotlin.Unit> getOnFocusChange();
property public abstract kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChange;
}
public final class FocusObserverModifierKt {
- method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focusObserver(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChange);
+ method public static androidx.compose.ui.Modifier focusObserver(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChange);
}
- @androidx.compose.ui.focus.ExperimentalFocus public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
+ public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
method public androidx.compose.ui.focus.FocusRequester getFocusRequester();
property public abstract androidx.compose.ui.focus.FocusRequester focusRequester;
}
public final class FocusRequesterModifierKt {
- method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focusRequester(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester);
+ method public static androidx.compose.ui.Modifier focusRequester(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester);
}
@androidx.compose.runtime.Stable public interface Modifier {
@@ -194,17 +197,17 @@
public final class AndroidAutofillTypeKt {
}
- public interface Autofill {
+ @androidx.compose.ui.ExperimentalComposeUiApi public interface Autofill {
method public void cancelAutofillForNode(androidx.compose.ui.autofill.AutofillNode autofillNode);
method public void requestAutofillForNode(androidx.compose.ui.autofill.AutofillNode autofillNode);
}
- public final class AutofillNode {
+ @androidx.compose.ui.ExperimentalComposeUiApi public final class AutofillNode {
ctor public AutofillNode(java.util.List<? extends androidx.compose.ui.autofill.AutofillType> autofillTypes, androidx.compose.ui.geometry.Rect? boundingBox, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit>? onFill);
method public java.util.List<androidx.compose.ui.autofill.AutofillType> component1();
method public androidx.compose.ui.geometry.Rect? component2();
method public kotlin.jvm.functions.Function1<java.lang.String,kotlin.Unit>? component3();
- method public androidx.compose.ui.autofill.AutofillNode copy(java.util.List<? extends androidx.compose.ui.autofill.AutofillType> autofillTypes, androidx.compose.ui.geometry.Rect? boundingBox, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit>? onFill);
+ method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.autofill.AutofillNode copy(java.util.List<? extends androidx.compose.ui.autofill.AutofillType> autofillTypes, androidx.compose.ui.geometry.Rect? boundingBox, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit>? onFill);
method public java.util.List<androidx.compose.ui.autofill.AutofillType> getAutofillTypes();
method public androidx.compose.ui.geometry.Rect? getBoundingBox();
method public int getId();
@@ -216,7 +219,7 @@
property public final kotlin.jvm.functions.Function1<java.lang.String,kotlin.Unit>? onFill;
}
- public final class AutofillTree {
+ @androidx.compose.ui.ExperimentalComposeUiApi public final class AutofillTree {
ctor public AutofillTree();
method public java.util.Map<java.lang.Integer,androidx.compose.ui.autofill.AutofillNode> getChildren();
method public kotlin.Unit? performAutofill(int id, String value);
@@ -224,7 +227,7 @@
property public final java.util.Map<java.lang.Integer,androidx.compose.ui.autofill.AutofillNode> children;
}
- public enum AutofillType {
+ @androidx.compose.ui.ExperimentalComposeUiApi public enum AutofillType {
enum_constant public static final androidx.compose.ui.autofill.AutofillType AddressAuxiliaryDetails;
enum_constant public static final androidx.compose.ui.autofill.AutofillType AddressCountry;
enum_constant public static final androidx.compose.ui.autofill.AutofillType AddressLocality;
@@ -328,27 +331,49 @@
package androidx.compose.ui.focus {
- @kotlin.RequiresOptIn(message="The Focus API is experimental and is likely to change in the future.") public @interface ExperimentalFocus {
- }
-
- @androidx.compose.ui.focus.ExperimentalFocus public interface FocusManager {
+ public interface FocusManager {
method public void clearFocus(optional boolean forcedClear);
}
public final class FocusNodeUtilsKt {
}
- @androidx.compose.ui.focus.ExperimentalFocus public final class FocusRequester {
+ public final class FocusRequester {
ctor public FocusRequester();
method public boolean captureFocus();
method public boolean freeFocus();
method public void requestFocus();
+ field public static final androidx.compose.ui.focus.FocusRequester.Companion Companion;
+ }
+
+ public static final class FocusRequester.Companion {
+ method public androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory createRefs();
+ }
+
+ public static final class FocusRequester.Companion.FocusRequesterFactory {
+ method public operator androidx.compose.ui.focus.FocusRequester component1();
+ method public operator androidx.compose.ui.focus.FocusRequester component10();
+ method public operator androidx.compose.ui.focus.FocusRequester component11();
+ method public operator androidx.compose.ui.focus.FocusRequester component12();
+ method public operator androidx.compose.ui.focus.FocusRequester component13();
+ method public operator androidx.compose.ui.focus.FocusRequester component14();
+ method public operator androidx.compose.ui.focus.FocusRequester component15();
+ method public operator androidx.compose.ui.focus.FocusRequester component16();
+ method public operator androidx.compose.ui.focus.FocusRequester component2();
+ method public operator androidx.compose.ui.focus.FocusRequester component3();
+ method public operator androidx.compose.ui.focus.FocusRequester component4();
+ method public operator androidx.compose.ui.focus.FocusRequester component5();
+ method public operator androidx.compose.ui.focus.FocusRequester component6();
+ method public operator androidx.compose.ui.focus.FocusRequester component7();
+ method public operator androidx.compose.ui.focus.FocusRequester component8();
+ method public operator androidx.compose.ui.focus.FocusRequester component9();
+ field public static final androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory INSTANCE;
}
public final class FocusRequesterKt {
}
- @androidx.compose.ui.focus.ExperimentalFocus public enum FocusState {
+ public enum FocusState {
enum_constant public static final androidx.compose.ui.focus.FocusState Active;
enum_constant public static final androidx.compose.ui.focus.FocusState ActiveParent;
enum_constant public static final androidx.compose.ui.focus.FocusState Captured;
@@ -410,9 +435,6 @@
method public static androidx.compose.ui.Modifier dragSlopExceededGestureFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function0<kotlin.Unit> onDragSlopExceeded, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.gesture.Direction,java.lang.Boolean>? canDrag, optional androidx.compose.ui.gesture.scrollorientationlocking.Orientation? orientation);
}
- @kotlin.RequiresOptIn(message="This pointer input API is experimental and is likely to change before becoming " + "stable.") public @interface ExperimentalPointerInput {
- }
-
public final class GestureUtilsKt {
method public static boolean anyPointersInBounds-5eFHUEc(java.util.List<androidx.compose.ui.input.pointer.PointerInputChange>, long bounds);
}
@@ -493,11 +515,11 @@
package androidx.compose.ui.gesture.customevents {
- @androidx.compose.ui.gesture.ExperimentalPointerInput public final class DelayUpEvent implements androidx.compose.ui.input.pointer.CustomEvent {
+ public final class DelayUpEvent implements androidx.compose.ui.input.pointer.CustomEvent {
ctor public DelayUpEvent(androidx.compose.ui.gesture.customevents.DelayUpMessage message, java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers);
method public androidx.compose.ui.gesture.customevents.DelayUpMessage component1();
method public java.util.Set<androidx.compose.ui.input.pointer.PointerId> component2();
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public androidx.compose.ui.gesture.customevents.DelayUpEvent copy(androidx.compose.ui.gesture.customevents.DelayUpMessage message, java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers);
+ method public androidx.compose.ui.gesture.customevents.DelayUpEvent copy(androidx.compose.ui.gesture.customevents.DelayUpMessage message, java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers);
method public androidx.compose.ui.gesture.customevents.DelayUpMessage getMessage();
method public java.util.Set<androidx.compose.ui.input.pointer.PointerId> getPointers();
method public void setMessage(androidx.compose.ui.gesture.customevents.DelayUpMessage p);
@@ -505,7 +527,7 @@
property public final java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers;
}
- @androidx.compose.ui.gesture.ExperimentalPointerInput public enum DelayUpMessage {
+ public enum DelayUpMessage {
enum_constant public static final androidx.compose.ui.gesture.customevents.DelayUpMessage DelayUp;
enum_constant public static final androidx.compose.ui.gesture.customevents.DelayUpMessage DelayedUpConsumed;
enum_constant public static final androidx.compose.ui.gesture.customevents.DelayUpMessage DelayedUpNotConsumed;
@@ -517,6 +539,37 @@
}
+package androidx.compose.ui.gesture.nestedscroll {
+
+ public interface NestedScrollConnection {
+ method public default void onPostFling-Pv53iXo(long consumed, long available, kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Velocity,kotlin.Unit> onFinished);
+ method public default long onPostScroll-l-UAZDg(long consumed, long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+ method public default long onPreFling-TH1AsA0(long available);
+ method public default long onPreScroll-vG6bCaM(long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+ }
+
+ public final class NestedScrollDelegatingWrapperKt {
+ }
+
+ public final class NestedScrollDispatcher {
+ ctor public NestedScrollDispatcher();
+ method public void dispatchPostFling-uYzo7IE(long consumed, long available);
+ method public long dispatchPostScroll-l-UAZDg(long consumed, long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+ method public long dispatchPreFling-TH1AsA0(long available);
+ method public long dispatchPreScroll-vG6bCaM(long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+ }
+
+ public final class NestedScrollModifierKt {
+ method public static androidx.compose.ui.Modifier nestedScroll(androidx.compose.ui.Modifier, androidx.compose.ui.gesture.nestedscroll.NestedScrollConnection connection, optional androidx.compose.ui.gesture.nestedscroll.NestedScrollDispatcher? dispatcher);
+ }
+
+ public enum NestedScrollSource {
+ enum_constant public static final androidx.compose.ui.gesture.nestedscroll.NestedScrollSource Drag;
+ enum_constant public static final androidx.compose.ui.gesture.nestedscroll.NestedScrollSource Fling;
+ }
+
+}
+
package androidx.compose.ui.gesture.scrollorientationlocking {
public enum Orientation {
@@ -524,7 +577,7 @@
enum_constant public static final androidx.compose.ui.gesture.scrollorientationlocking.Orientation Vertical;
}
- @androidx.compose.ui.gesture.ExperimentalPointerInput public final class ScrollOrientationLocker {
+ public final class ScrollOrientationLocker {
ctor public ScrollOrientationLocker(androidx.compose.ui.input.pointer.CustomEventDispatcher customEventDispatcher);
method public void attemptToLockPointers(java.util.List<androidx.compose.ui.input.pointer.PointerInputChange> changes, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation);
method public java.util.List<androidx.compose.ui.input.pointer.PointerInputChange> getPointersFor(java.util.List<androidx.compose.ui.input.pointer.PointerInputChange> changes, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation);
@@ -678,7 +731,8 @@
public final class VectorApplier extends androidx.compose.runtime.AbstractApplier<androidx.compose.ui.graphics.vector.VNode> {
ctor public VectorApplier(androidx.compose.ui.graphics.vector.VNode root);
- method public void insert(int index, androidx.compose.ui.graphics.vector.VNode instance);
+ method public void insertBottomUp(int index, androidx.compose.ui.graphics.vector.VNode instance);
+ method public void insertTopDown(int index, androidx.compose.ui.graphics.vector.VNode instance);
method public void move(int from, int to, int count);
method protected void onClear();
method public void remove(int index, int count);
@@ -811,7 +865,7 @@
package androidx.compose.ui.input.key {
- @Deprecated @androidx.compose.ui.input.key.ExperimentalKeyInput public interface Alt {
+ @Deprecated public interface Alt {
method @Deprecated public boolean isLeftAltPressed();
method @Deprecated public default boolean isPressed();
method @Deprecated public boolean isRightAltPressed();
@@ -820,9 +874,6 @@
property public abstract boolean isRightAltPressed;
}
- @kotlin.RequiresOptIn(message="The Key Input API is experimental and is likely to change in the future.") public @interface ExperimentalKeyInput {
- }
-
public final inline class Key {
ctor public Key();
method public static int constructor-impl(int keyCode);
@@ -1416,7 +1467,7 @@
property public final int ZoomOut;
}
- @androidx.compose.ui.input.key.ExperimentalKeyInput public interface KeyEvent {
+ public interface KeyEvent {
method @Deprecated public androidx.compose.ui.input.key.Alt getAlt();
method public int getKey-EK5gGoQ();
method public androidx.compose.ui.input.key.KeyEventType getType();
@@ -1435,15 +1486,15 @@
property public abstract int utf16CodePoint;
}
- @androidx.compose.ui.input.key.ExperimentalKeyInput public enum KeyEventType {
+ public enum KeyEventType {
enum_constant public static final androidx.compose.ui.input.key.KeyEventType KeyDown;
enum_constant public static final androidx.compose.ui.input.key.KeyEventType KeyUp;
enum_constant public static final androidx.compose.ui.input.key.KeyEventType Unknown;
}
public final class KeyInputModifierKt {
- method @androidx.compose.ui.input.key.ExperimentalKeyInput public static androidx.compose.ui.Modifier keyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onKeyEvent);
- method @androidx.compose.ui.input.key.ExperimentalKeyInput public static androidx.compose.ui.Modifier previewKeyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
+ method public static androidx.compose.ui.Modifier keyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onKeyEvent);
+ method public static androidx.compose.ui.Modifier previewKeyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
}
}
@@ -1472,8 +1523,7 @@
method public void retainHitPaths(java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointerIds);
}
- @androidx.compose.ui.gesture.ExperimentalPointerInput @kotlin.coroutines.RestrictsSuspension public interface HandlePointerInputScope extends androidx.compose.ui.unit.Density {
- method public suspend Object? awaitCustomEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.CustomEvent> p);
+ @kotlin.coroutines.RestrictsSuspension public interface HandlePointerInputScope extends androidx.compose.ui.unit.Density {
method public suspend Object? awaitPointerEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerEvent> p);
method public androidx.compose.ui.input.pointer.PointerEvent getCurrentEvent();
method public long getSize-YbymL2g();
@@ -1600,12 +1650,10 @@
property public abstract androidx.compose.ui.input.pointer.PointerInputFilter pointerInputFilter;
}
- @androidx.compose.ui.gesture.ExperimentalPointerInput public interface PointerInputScope extends androidx.compose.ui.unit.Density {
- method public androidx.compose.ui.input.pointer.CustomEventDispatcher getCustomEventDispatcher();
+ public interface PointerInputScope extends androidx.compose.ui.unit.Density {
method public long getSize-YbymL2g();
method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
method public suspend <R> Object? handlePointerInput(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.HandlePointerInputScope,? super kotlin.coroutines.Continuation<? super R>,?> handler, kotlin.coroutines.Continuation<? super R> p);
- property public abstract androidx.compose.ui.input.pointer.CustomEventDispatcher customEventDispatcher;
property public abstract long size;
property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
}
@@ -1626,7 +1674,7 @@
}
public final class SuspendingPointerInputFilterKt {
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
}
}
@@ -1739,6 +1787,22 @@
property public abstract Object layoutId;
}
+ public interface LayoutInfo {
+ method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
+ method public int getHeight();
+ method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
+ method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
+ method public int getWidth();
+ method public boolean isAttached();
+ method public boolean isPlaced();
+ property public abstract androidx.compose.ui.layout.LayoutCoordinates coordinates;
+ property public abstract int height;
+ property public abstract boolean isAttached;
+ property public abstract boolean isPlaced;
+ property public abstract androidx.compose.ui.layout.LayoutInfo? parentInfo;
+ property public abstract int width;
+ }
+
public final class LayoutKt {
method @androidx.compose.runtime.Composable public static void Layout(kotlin.jvm.functions.Function0<kotlin.Unit> content, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicHeightMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicHeightMeasureBlock, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
method @androidx.compose.runtime.Composable public static void Layout(kotlin.jvm.functions.Function0<kotlin.Unit> content, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
@@ -1794,6 +1858,16 @@
method public static inline String! toString-impl(androidx.compose.ui.layout.Placeable! p);
}
+ public final class ModifierInfo {
+ ctor public ModifierInfo(androidx.compose.ui.Modifier modifier, androidx.compose.ui.layout.LayoutCoordinates coordinates, Object? extra);
+ method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
+ method public Object? getExtra();
+ method public androidx.compose.ui.Modifier getModifier();
+ property public final androidx.compose.ui.layout.LayoutCoordinates coordinates;
+ property public final Object? extra;
+ property public final androidx.compose.ui.Modifier modifier;
+ }
+
public interface OnGloballyPositionedModifier extends androidx.compose.ui.Modifier.Element {
method public void onGloballyPositioned(androidx.compose.ui.layout.LayoutCoordinates coordinates);
}
@@ -1899,6 +1973,9 @@
method public java.util.List<androidx.compose.ui.layout.Measurable> subcompose(Object? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
+ public final class TestModifierUpdaterKt {
+ }
+
public final class VerticalAlignmentLine extends androidx.compose.ui.layout.AlignmentLine {
ctor public VerticalAlignmentLine(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Integer> merger);
}
@@ -1923,25 +2000,15 @@
@kotlin.RequiresOptIn(message="This API is internal to library.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface InternalCoreApi {
}
- public final class LayoutNode implements androidx.compose.ui.layout.Measurable androidx.compose.ui.node.OwnerScope androidx.compose.ui.layout.Remeasurement {
- ctor public LayoutNode();
- method public void attach(androidx.compose.ui.node.Owner owner);
- method public void detach();
+ public final class LayoutNode implements androidx.compose.ui.layout.LayoutInfo androidx.compose.ui.layout.Measurable androidx.compose.ui.node.OwnerScope androidx.compose.ui.layout.Remeasurement {
method public void forceRemeasure();
- method @Deprecated public boolean getCanMultiMeasure();
- method public java.util.List<androidx.compose.ui.node.LayoutNode> getChildren();
method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
- method public androidx.compose.ui.unit.Density getDensity();
- method public int getDepth();
method public int getHeight();
- method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
- method public androidx.compose.ui.node.MeasureBlocks getMeasureBlocks();
- method public androidx.compose.ui.Modifier getModifier();
- method public java.util.List<androidx.compose.ui.node.ModifierInfo> getModifierInfo();
- method public androidx.compose.ui.node.Owner? getOwner();
- method public androidx.compose.ui.node.LayoutNode? getParent();
+ method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
method public Object? getParentData();
+ method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
method public int getWidth();
+ method public boolean isAttached();
method public boolean isPlaced();
method public boolean isValid();
method public int maxIntrinsicHeight(int width);
@@ -1949,28 +2016,14 @@
method public androidx.compose.ui.layout.Placeable measure-BRTryo0(long constraints);
method public int minIntrinsicHeight(int width);
method public int minIntrinsicWidth(int height);
- method public void place(int x, int y);
- method @Deprecated public void setCanMultiMeasure(boolean p);
- method public void setDensity(androidx.compose.ui.unit.Density p);
- method public void setDepth(int p);
- method public void setLayoutDirection(androidx.compose.ui.unit.LayoutDirection value);
- method public void setMeasureBlocks(androidx.compose.ui.node.MeasureBlocks value);
- method public void setModifier(androidx.compose.ui.Modifier value);
- property @Deprecated public final boolean canMultiMeasure;
- property public final java.util.List<androidx.compose.ui.node.LayoutNode> children;
- property public final androidx.compose.ui.layout.LayoutCoordinates coordinates;
- property public final androidx.compose.ui.unit.Density density;
- property public final int depth;
- property public final int height;
- property public final boolean isPlaced;
+ property public androidx.compose.ui.layout.LayoutCoordinates coordinates;
+ property public int height;
+ property public boolean isAttached;
+ property public boolean isPlaced;
property public boolean isValid;
- property public final androidx.compose.ui.unit.LayoutDirection layoutDirection;
- property public final androidx.compose.ui.node.MeasureBlocks measureBlocks;
- property public final androidx.compose.ui.Modifier modifier;
- property public final androidx.compose.ui.node.Owner? owner;
- property public final androidx.compose.ui.node.LayoutNode? parent;
property public Object? parentData;
- property public final int width;
+ property public androidx.compose.ui.layout.LayoutInfo? parentInfo;
+ property public int width;
}
public final class LayoutNodeKt {
@@ -1984,16 +2037,6 @@
method public int minIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int h);
}
- public final class ModifierInfo {
- ctor public ModifierInfo(androidx.compose.ui.Modifier modifier, androidx.compose.ui.layout.LayoutCoordinates coordinates, Object? extra);
- method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
- method public Object? getExtra();
- method public androidx.compose.ui.Modifier getModifier();
- property public final androidx.compose.ui.layout.LayoutCoordinates coordinates;
- property public final Object? extra;
- property public final androidx.compose.ui.Modifier modifier;
- }
-
public interface OwnedLayer {
method public void destroy();
method public void drawLayer(androidx.compose.ui.graphics.Canvas canvas);
@@ -2017,7 +2060,6 @@
method public androidx.compose.ui.focus.FocusManager getFocusManager();
method public androidx.compose.ui.text.font.Font.ResourceLoader getFontLoader();
method public androidx.compose.ui.hapticfeedback.HapticFeedback getHapticFeedBack();
- method public boolean getHasPendingMeasureOrLayout();
method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
method public long getMeasureIteration();
method public androidx.compose.ui.node.LayoutNode getRoot();
@@ -2035,7 +2077,7 @@
method public void onRequestRelayout(androidx.compose.ui.node.LayoutNode layoutNode);
method public void onSemanticsChange();
method public boolean requestFocus();
- method @androidx.compose.ui.input.key.ExperimentalKeyInput public boolean sendKeyEvent(androidx.compose.ui.input.key.KeyEvent keyEvent);
+ method public boolean sendKeyEvent(androidx.compose.ui.input.key.KeyEvent keyEvent);
property public abstract androidx.compose.ui.autofill.Autofill? autofill;
property public abstract androidx.compose.ui.autofill.AutofillTree autofillTree;
property public abstract androidx.compose.ui.platform.ClipboardManager clipboardManager;
@@ -2043,7 +2085,6 @@
property public abstract androidx.compose.ui.focus.FocusManager focusManager;
property public abstract androidx.compose.ui.text.font.Font.ResourceLoader fontLoader;
property public abstract androidx.compose.ui.hapticfeedback.HapticFeedback hapticFeedBack;
- property public abstract boolean hasPendingMeasureOrLayout;
property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
property public abstract long measureIteration;
property public abstract androidx.compose.ui.node.LayoutNode root;
@@ -2079,16 +2120,13 @@
property public final T? value;
}
- public final class UiApplier implements androidx.compose.runtime.Applier<java.lang.Object> {
+ public final class UiApplier extends androidx.compose.runtime.AbstractApplier<java.lang.Object> {
ctor public UiApplier(Object root);
- method public void clear();
- method public void down(Object node);
- method public Object getCurrent();
- method public void insert(int index, Object instance);
+ method public void insertBottomUp(int index, Object instance);
+ method public void insertTopDown(int index, Object instance);
method public void move(int from, int to, int count);
+ method protected void onClear();
method public void remove(int index, int count);
- method public void up();
- property public Object current;
}
public final class ViewInteropKt {
@@ -2126,8 +2164,6 @@
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ViewConfiguration> getAmbientViewConfiguration();
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowManager> getAmbientWindowManager();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.animation.core.AnimationClockObservable>! getAnimationClockAmbient();
- method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.Autofill>! getAutofillAmbient();
- method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.AutofillTree>! getAutofillTreeAmbient();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ClipboardManager>! getClipboardManagerAmbient();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.unit.Density>! getDensityAmbient();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.focus.FocusManager>! getFocusManagerAmbient();
@@ -2159,31 +2195,6 @@
public final class AndroidComposeViewKt {
}
- public interface AndroidOwner extends androidx.compose.ui.node.Owner {
- method public void addAndroidView(androidx.compose.ui.viewinterop.AndroidViewHolder view, androidx.compose.ui.node.LayoutNode layoutNode);
- method public void drawAndroidView(androidx.compose.ui.viewinterop.AndroidViewHolder view, android.graphics.Canvas canvas);
- method public kotlin.jvm.functions.Function1<android.content.res.Configuration,kotlin.Unit> getConfigurationChangeObserver();
- method public android.view.View getView();
- method public androidx.compose.ui.platform.AndroidOwner.ViewTreeOwners? getViewTreeOwners();
- method public void invalidateDescendants();
- method public void removeAndroidView(androidx.compose.ui.viewinterop.AndroidViewHolder view);
- method public void setConfigurationChangeObserver(kotlin.jvm.functions.Function1<? super android.content.res.Configuration,kotlin.Unit> p);
- method public void setOnViewTreeOwnersAvailable(kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.AndroidOwner.ViewTreeOwners,kotlin.Unit> callback);
- property public abstract kotlin.jvm.functions.Function1<android.content.res.Configuration,kotlin.Unit> configurationChangeObserver;
- property public abstract android.view.View view;
- property public abstract androidx.compose.ui.platform.AndroidOwner.ViewTreeOwners? viewTreeOwners;
- }
-
- public static final class AndroidOwner.ViewTreeOwners {
- ctor public AndroidOwner.ViewTreeOwners(androidx.lifecycle.LifecycleOwner lifecycleOwner, androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, androidx.savedstate.SavedStateRegistryOwner savedStateRegistryOwner);
- method public androidx.lifecycle.LifecycleOwner getLifecycleOwner();
- method public androidx.savedstate.SavedStateRegistryOwner getSavedStateRegistryOwner();
- method public androidx.lifecycle.ViewModelStoreOwner getViewModelStoreOwner();
- property public final androidx.lifecycle.LifecycleOwner lifecycleOwner;
- property public final androidx.savedstate.SavedStateRegistryOwner savedStateRegistryOwner;
- property public final androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner;
- }
-
public final class AndroidUriHandler implements androidx.compose.ui.platform.UriHandler {
ctor public AndroidUriHandler(android.content.Context context);
method public void openUri(String uri);
@@ -2262,10 +2273,6 @@
public final class JvmActualsKt {
}
- public final class SubcompositionKt {
- method @MainThread public static androidx.compose.runtime.Composition subcomposeInto(androidx.compose.ui.node.LayoutNode container, androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> composable);
- }
-
public final class TestTagKt {
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier testTag(androidx.compose.ui.Modifier, String tag);
}
@@ -2317,6 +2324,23 @@
property public abstract float touchSlop;
}
+ @VisibleForTesting public interface ViewRootForTest extends androidx.compose.ui.node.Owner {
+ method public boolean getHasPendingMeasureOrLayout();
+ method public android.view.View getView();
+ method public void invalidateDescendants();
+ method public boolean isLifecycleInResumedState();
+ property public abstract boolean hasPendingMeasureOrLayout;
+ property public abstract boolean isLifecycleInResumedState;
+ property public abstract android.view.View view;
+ field public static final androidx.compose.ui.platform.ViewRootForTest.Companion Companion;
+ }
+
+ public static final class ViewRootForTest.Companion {
+ method public kotlin.jvm.functions.Function1<androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? getOnViewCreatedCallback();
+ method public void setOnViewCreatedCallback(kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? p);
+ property public final kotlin.jvm.functions.Function1<androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? onViewCreatedCallback;
+ }
+
@androidx.compose.runtime.Stable public interface WindowManager {
method public boolean isWindowFocused();
property public abstract boolean isWindowFocused;
@@ -2412,7 +2436,7 @@
package androidx.compose.ui.selection {
- public interface Selectable {
+ @androidx.compose.ui.text.ExperimentalTextApi public interface Selectable {
method public androidx.compose.ui.geometry.Rect getBoundingBox(int offset);
method public long getHandlePosition-F1C5BW0(androidx.compose.ui.selection.Selection selection, boolean isStartHandle);
method public androidx.compose.ui.layout.LayoutCoordinates? getLayoutCoordinates();
@@ -2462,9 +2486,11 @@
public final class SelectionManagerKt {
}
- public interface SelectionRegistrar {
- method public void onPositionChange();
- method public void onUpdateSelection-rULFVbc(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition, long endPosition);
+ @androidx.compose.ui.text.ExperimentalTextApi public interface SelectionRegistrar {
+ method public void notifyPositionChange();
+ method public void notifySelectionUpdate-rULFVbc(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition, long endPosition);
+ method public void notifySelectionUpdateEnd();
+ method public void notifySelectionUpdateStart-YJiYy8w(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition);
method public androidx.compose.ui.selection.Selectable subscribe(androidx.compose.ui.selection.Selectable selectable);
method public void unsubscribe(androidx.compose.ui.selection.Selectable selectable);
}
@@ -2612,8 +2638,9 @@
method public androidx.compose.ui.geometry.Rect getGlobalBounds();
method public long getGlobalPosition-F1C5BW0();
method public int getId();
- method public androidx.compose.ui.node.LayoutNode getLayoutNode();
+ method public androidx.compose.ui.layout.LayoutInfo getLayoutInfo();
method public boolean getMergingEnabled();
+ method public androidx.compose.ui.node.Owner? getOwner();
method public androidx.compose.ui.semantics.SemanticsNode? getParent();
method public long getPositionInRoot-F1C5BW0();
method public long getSize-YbymL2g();
@@ -2625,8 +2652,9 @@
property public final long globalPosition;
property public final int id;
property public final boolean isRoot;
- property public final androidx.compose.ui.node.LayoutNode layoutNode;
+ property public final androidx.compose.ui.layout.LayoutInfo layoutInfo;
property public final boolean mergingEnabled;
+ property public final androidx.compose.ui.node.Owner? owner;
property public final androidx.compose.ui.semantics.SemanticsNode? parent;
property public final long positionInRoot;
property public final long size;
@@ -2649,9 +2677,8 @@
}
public final class SemanticsProperties {
- method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getAccessibilityLabel();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityRangeInfo> getAccessibilityRangeInfo();
- method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getAccessibilityValue();
+ method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getContentDescription();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getDisabled();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> getFocused();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getHidden();
@@ -2660,14 +2687,14 @@
method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getIsDialog();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getIsPopup();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> getSelected();
+ method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getStateDescription();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getTestTag();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.AnnotatedString> getText();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.TextRange> getTextSelectionRange();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.state.ToggleableState> getToggleableState();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityScrollState> getVerticalAccessibilityScrollState();
- property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> AccessibilityLabel;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityRangeInfo> AccessibilityRangeInfo;
- property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> AccessibilityValue;
+ property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> ContentDescription;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> Disabled;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> Focused;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> Hidden;
@@ -2676,6 +2703,7 @@
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> IsDialog;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> IsPopup;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> Selected;
+ property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> StateDescription;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> TestTag;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.AnnotatedString> Text;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.TextRange> TextSelectionRange;
@@ -2690,14 +2718,16 @@
method public static void dialog(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void disabled(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void dismiss(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
- method public static String getAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
- method public static String getAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
- method public static androidx.compose.ui.semantics.AccessibilityRangeInfo getAccessibilityValueRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method @Deprecated public static String getAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method @Deprecated public static String getAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method public static String getContentDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction> getCustomActions(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static boolean getFocused(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static androidx.compose.ui.semantics.AccessibilityScrollState getHorizontalAccessibilityScrollState(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static androidx.compose.ui.text.input.ImeAction getImeAction(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static boolean getSelected(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method public static String getStateDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method public static androidx.compose.ui.semantics.AccessibilityRangeInfo getStateDescriptionRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static String getTestTag(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static androidx.compose.ui.text.AnnotatedString getText(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void getTextLayoutResult(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.text.TextLayoutResult>,java.lang.Boolean> action);
@@ -2710,9 +2740,9 @@
method public static void pasteText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
method public static void popup(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void scrollBy(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> action);
- method public static void setAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
- method public static void setAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
- method public static void setAccessibilityValueRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.semantics.AccessibilityRangeInfo p);
+ method @Deprecated public static void setAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
+ method @Deprecated public static void setAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
+ method public static void setContentDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
method public static void setCustomActions(androidx.compose.ui.semantics.SemanticsPropertyReceiver, java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction> p);
method public static void setFocused(androidx.compose.ui.semantics.SemanticsPropertyReceiver, boolean p);
method public static void setHorizontalAccessibilityScrollState(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.semantics.AccessibilityScrollState p);
@@ -2720,6 +2750,8 @@
method public static void setProgress(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> action);
method public static void setSelected(androidx.compose.ui.semantics.SemanticsPropertyReceiver, boolean p);
method public static void setSelection(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super java.lang.Integer,? super java.lang.Boolean,java.lang.Boolean> action);
+ method public static void setStateDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
+ method public static void setStateDescriptionRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.semantics.AccessibilityRangeInfo p);
method public static void setTestTag(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
method public static void setText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.text.AnnotatedString p);
method public static void setText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString,java.lang.Boolean> action);
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index c05fc73..9f00a14c 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -132,26 +132,29 @@
method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
}
- public final class FocusModifierKt {
- method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focus(androidx.compose.ui.Modifier);
+ @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalComposeUiApi {
}
- @androidx.compose.ui.focus.ExperimentalFocus public interface FocusObserverModifier extends androidx.compose.ui.Modifier.Element {
+ public final class FocusModifierKt {
+ method public static androidx.compose.ui.Modifier focus(androidx.compose.ui.Modifier);
+ }
+
+ public interface FocusObserverModifier extends androidx.compose.ui.Modifier.Element {
method public kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusState,kotlin.Unit> getOnFocusChange();
property public abstract kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChange;
}
public final class FocusObserverModifierKt {
- method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focusObserver(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChange);
+ method public static androidx.compose.ui.Modifier focusObserver(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChange);
}
- @androidx.compose.ui.focus.ExperimentalFocus public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
+ public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
method public androidx.compose.ui.focus.FocusRequester getFocusRequester();
property public abstract androidx.compose.ui.focus.FocusRequester focusRequester;
}
public final class FocusRequesterModifierKt {
- method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focusRequester(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester);
+ method public static androidx.compose.ui.Modifier focusRequester(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester);
}
@androidx.compose.runtime.Stable public interface Modifier {
@@ -194,17 +197,17 @@
public final class AndroidAutofillTypeKt {
}
- public interface Autofill {
+ @androidx.compose.ui.ExperimentalComposeUiApi public interface Autofill {
method public void cancelAutofillForNode(androidx.compose.ui.autofill.AutofillNode autofillNode);
method public void requestAutofillForNode(androidx.compose.ui.autofill.AutofillNode autofillNode);
}
- public final class AutofillNode {
+ @androidx.compose.ui.ExperimentalComposeUiApi public final class AutofillNode {
ctor public AutofillNode(java.util.List<? extends androidx.compose.ui.autofill.AutofillType> autofillTypes, androidx.compose.ui.geometry.Rect? boundingBox, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit>? onFill);
method public java.util.List<androidx.compose.ui.autofill.AutofillType> component1();
method public androidx.compose.ui.geometry.Rect? component2();
method public kotlin.jvm.functions.Function1<java.lang.String,kotlin.Unit>? component3();
- method public androidx.compose.ui.autofill.AutofillNode copy(java.util.List<? extends androidx.compose.ui.autofill.AutofillType> autofillTypes, androidx.compose.ui.geometry.Rect? boundingBox, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit>? onFill);
+ method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.autofill.AutofillNode copy(java.util.List<? extends androidx.compose.ui.autofill.AutofillType> autofillTypes, androidx.compose.ui.geometry.Rect? boundingBox, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit>? onFill);
method public java.util.List<androidx.compose.ui.autofill.AutofillType> getAutofillTypes();
method public androidx.compose.ui.geometry.Rect? getBoundingBox();
method public int getId();
@@ -216,7 +219,7 @@
property public final kotlin.jvm.functions.Function1<java.lang.String,kotlin.Unit>? onFill;
}
- public final class AutofillTree {
+ @androidx.compose.ui.ExperimentalComposeUiApi public final class AutofillTree {
ctor public AutofillTree();
method public java.util.Map<java.lang.Integer,androidx.compose.ui.autofill.AutofillNode> getChildren();
method public kotlin.Unit? performAutofill(int id, String value);
@@ -224,7 +227,7 @@
property public final java.util.Map<java.lang.Integer,androidx.compose.ui.autofill.AutofillNode> children;
}
- public enum AutofillType {
+ @androidx.compose.ui.ExperimentalComposeUiApi public enum AutofillType {
enum_constant public static final androidx.compose.ui.autofill.AutofillType AddressAuxiliaryDetails;
enum_constant public static final androidx.compose.ui.autofill.AutofillType AddressCountry;
enum_constant public static final androidx.compose.ui.autofill.AutofillType AddressLocality;
@@ -328,27 +331,49 @@
package androidx.compose.ui.focus {
- @kotlin.RequiresOptIn(message="The Focus API is experimental and is likely to change in the future.") public @interface ExperimentalFocus {
- }
-
- @androidx.compose.ui.focus.ExperimentalFocus public interface FocusManager {
+ public interface FocusManager {
method public void clearFocus(optional boolean forcedClear);
}
public final class FocusNodeUtilsKt {
}
- @androidx.compose.ui.focus.ExperimentalFocus public final class FocusRequester {
+ public final class FocusRequester {
ctor public FocusRequester();
method public boolean captureFocus();
method public boolean freeFocus();
method public void requestFocus();
+ field public static final androidx.compose.ui.focus.FocusRequester.Companion Companion;
+ }
+
+ public static final class FocusRequester.Companion {
+ method public androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory createRefs();
+ }
+
+ public static final class FocusRequester.Companion.FocusRequesterFactory {
+ method public operator androidx.compose.ui.focus.FocusRequester component1();
+ method public operator androidx.compose.ui.focus.FocusRequester component10();
+ method public operator androidx.compose.ui.focus.FocusRequester component11();
+ method public operator androidx.compose.ui.focus.FocusRequester component12();
+ method public operator androidx.compose.ui.focus.FocusRequester component13();
+ method public operator androidx.compose.ui.focus.FocusRequester component14();
+ method public operator androidx.compose.ui.focus.FocusRequester component15();
+ method public operator androidx.compose.ui.focus.FocusRequester component16();
+ method public operator androidx.compose.ui.focus.FocusRequester component2();
+ method public operator androidx.compose.ui.focus.FocusRequester component3();
+ method public operator androidx.compose.ui.focus.FocusRequester component4();
+ method public operator androidx.compose.ui.focus.FocusRequester component5();
+ method public operator androidx.compose.ui.focus.FocusRequester component6();
+ method public operator androidx.compose.ui.focus.FocusRequester component7();
+ method public operator androidx.compose.ui.focus.FocusRequester component8();
+ method public operator androidx.compose.ui.focus.FocusRequester component9();
+ field public static final androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory INSTANCE;
}
public final class FocusRequesterKt {
}
- @androidx.compose.ui.focus.ExperimentalFocus public enum FocusState {
+ public enum FocusState {
enum_constant public static final androidx.compose.ui.focus.FocusState Active;
enum_constant public static final androidx.compose.ui.focus.FocusState ActiveParent;
enum_constant public static final androidx.compose.ui.focus.FocusState Captured;
@@ -410,9 +435,6 @@
method public static androidx.compose.ui.Modifier dragSlopExceededGestureFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function0<kotlin.Unit> onDragSlopExceeded, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.gesture.Direction,java.lang.Boolean>? canDrag, optional androidx.compose.ui.gesture.scrollorientationlocking.Orientation? orientation);
}
- @kotlin.RequiresOptIn(message="This pointer input API is experimental and is likely to change before becoming " + "stable.") public @interface ExperimentalPointerInput {
- }
-
public final class GestureUtilsKt {
method public static boolean anyPointersInBounds-5eFHUEc(java.util.List<androidx.compose.ui.input.pointer.PointerInputChange>, long bounds);
}
@@ -493,11 +515,11 @@
package androidx.compose.ui.gesture.customevents {
- @androidx.compose.ui.gesture.ExperimentalPointerInput public final class DelayUpEvent implements androidx.compose.ui.input.pointer.CustomEvent {
+ public final class DelayUpEvent implements androidx.compose.ui.input.pointer.CustomEvent {
ctor public DelayUpEvent(androidx.compose.ui.gesture.customevents.DelayUpMessage message, java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers);
method public androidx.compose.ui.gesture.customevents.DelayUpMessage component1();
method public java.util.Set<androidx.compose.ui.input.pointer.PointerId> component2();
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public androidx.compose.ui.gesture.customevents.DelayUpEvent copy(androidx.compose.ui.gesture.customevents.DelayUpMessage message, java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers);
+ method public androidx.compose.ui.gesture.customevents.DelayUpEvent copy(androidx.compose.ui.gesture.customevents.DelayUpMessage message, java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers);
method public androidx.compose.ui.gesture.customevents.DelayUpMessage getMessage();
method public java.util.Set<androidx.compose.ui.input.pointer.PointerId> getPointers();
method public void setMessage(androidx.compose.ui.gesture.customevents.DelayUpMessage p);
@@ -505,7 +527,7 @@
property public final java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers;
}
- @androidx.compose.ui.gesture.ExperimentalPointerInput public enum DelayUpMessage {
+ public enum DelayUpMessage {
enum_constant public static final androidx.compose.ui.gesture.customevents.DelayUpMessage DelayUp;
enum_constant public static final androidx.compose.ui.gesture.customevents.DelayUpMessage DelayedUpConsumed;
enum_constant public static final androidx.compose.ui.gesture.customevents.DelayUpMessage DelayedUpNotConsumed;
@@ -517,6 +539,37 @@
}
+package androidx.compose.ui.gesture.nestedscroll {
+
+ public interface NestedScrollConnection {
+ method public default void onPostFling-Pv53iXo(long consumed, long available, kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Velocity,kotlin.Unit> onFinished);
+ method public default long onPostScroll-l-UAZDg(long consumed, long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+ method public default long onPreFling-TH1AsA0(long available);
+ method public default long onPreScroll-vG6bCaM(long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+ }
+
+ public final class NestedScrollDelegatingWrapperKt {
+ }
+
+ public final class NestedScrollDispatcher {
+ ctor public NestedScrollDispatcher();
+ method public void dispatchPostFling-uYzo7IE(long consumed, long available);
+ method public long dispatchPostScroll-l-UAZDg(long consumed, long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+ method public long dispatchPreFling-TH1AsA0(long available);
+ method public long dispatchPreScroll-vG6bCaM(long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+ }
+
+ public final class NestedScrollModifierKt {
+ method public static androidx.compose.ui.Modifier nestedScroll(androidx.compose.ui.Modifier, androidx.compose.ui.gesture.nestedscroll.NestedScrollConnection connection, optional androidx.compose.ui.gesture.nestedscroll.NestedScrollDispatcher? dispatcher);
+ }
+
+ public enum NestedScrollSource {
+ enum_constant public static final androidx.compose.ui.gesture.nestedscroll.NestedScrollSource Drag;
+ enum_constant public static final androidx.compose.ui.gesture.nestedscroll.NestedScrollSource Fling;
+ }
+
+}
+
package androidx.compose.ui.gesture.scrollorientationlocking {
public enum Orientation {
@@ -524,7 +577,7 @@
enum_constant public static final androidx.compose.ui.gesture.scrollorientationlocking.Orientation Vertical;
}
- @androidx.compose.ui.gesture.ExperimentalPointerInput public final class ScrollOrientationLocker {
+ public final class ScrollOrientationLocker {
ctor public ScrollOrientationLocker(androidx.compose.ui.input.pointer.CustomEventDispatcher customEventDispatcher);
method public void attemptToLockPointers(java.util.List<androidx.compose.ui.input.pointer.PointerInputChange> changes, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation);
method public java.util.List<androidx.compose.ui.input.pointer.PointerInputChange> getPointersFor(java.util.List<androidx.compose.ui.input.pointer.PointerInputChange> changes, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation);
@@ -678,7 +731,8 @@
public final class VectorApplier extends androidx.compose.runtime.AbstractApplier<androidx.compose.ui.graphics.vector.VNode> {
ctor public VectorApplier(androidx.compose.ui.graphics.vector.VNode root);
- method public void insert(int index, androidx.compose.ui.graphics.vector.VNode instance);
+ method public void insertBottomUp(int index, androidx.compose.ui.graphics.vector.VNode instance);
+ method public void insertTopDown(int index, androidx.compose.ui.graphics.vector.VNode instance);
method public void move(int from, int to, int count);
method protected void onClear();
method public void remove(int index, int count);
@@ -811,7 +865,7 @@
package androidx.compose.ui.input.key {
- @Deprecated @androidx.compose.ui.input.key.ExperimentalKeyInput public interface Alt {
+ @Deprecated public interface Alt {
method @Deprecated public boolean isLeftAltPressed();
method @Deprecated public default boolean isPressed();
method @Deprecated public boolean isRightAltPressed();
@@ -820,9 +874,6 @@
property public abstract boolean isRightAltPressed;
}
- @kotlin.RequiresOptIn(message="The Key Input API is experimental and is likely to change in the future.") public @interface ExperimentalKeyInput {
- }
-
public final inline class Key {
ctor public Key();
method public static int constructor-impl(int keyCode);
@@ -1416,7 +1467,7 @@
property public final int ZoomOut;
}
- @androidx.compose.ui.input.key.ExperimentalKeyInput public interface KeyEvent {
+ public interface KeyEvent {
method @Deprecated public androidx.compose.ui.input.key.Alt getAlt();
method public int getKey-EK5gGoQ();
method public androidx.compose.ui.input.key.KeyEventType getType();
@@ -1435,15 +1486,15 @@
property public abstract int utf16CodePoint;
}
- @androidx.compose.ui.input.key.ExperimentalKeyInput public enum KeyEventType {
+ public enum KeyEventType {
enum_constant public static final androidx.compose.ui.input.key.KeyEventType KeyDown;
enum_constant public static final androidx.compose.ui.input.key.KeyEventType KeyUp;
enum_constant public static final androidx.compose.ui.input.key.KeyEventType Unknown;
}
public final class KeyInputModifierKt {
- method @androidx.compose.ui.input.key.ExperimentalKeyInput public static androidx.compose.ui.Modifier keyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onKeyEvent);
- method @androidx.compose.ui.input.key.ExperimentalKeyInput public static androidx.compose.ui.Modifier previewKeyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
+ method public static androidx.compose.ui.Modifier keyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onKeyEvent);
+ method public static androidx.compose.ui.Modifier previewKeyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
}
}
@@ -1472,8 +1523,7 @@
method public void retainHitPaths(java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointerIds);
}
- @androidx.compose.ui.gesture.ExperimentalPointerInput @kotlin.coroutines.RestrictsSuspension public interface HandlePointerInputScope extends androidx.compose.ui.unit.Density {
- method public suspend Object? awaitCustomEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.CustomEvent> p);
+ @kotlin.coroutines.RestrictsSuspension public interface HandlePointerInputScope extends androidx.compose.ui.unit.Density {
method public suspend Object? awaitPointerEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerEvent> p);
method public androidx.compose.ui.input.pointer.PointerEvent getCurrentEvent();
method public long getSize-YbymL2g();
@@ -1600,12 +1650,10 @@
property public abstract androidx.compose.ui.input.pointer.PointerInputFilter pointerInputFilter;
}
- @androidx.compose.ui.gesture.ExperimentalPointerInput public interface PointerInputScope extends androidx.compose.ui.unit.Density {
- method public androidx.compose.ui.input.pointer.CustomEventDispatcher getCustomEventDispatcher();
+ public interface PointerInputScope extends androidx.compose.ui.unit.Density {
method public long getSize-YbymL2g();
method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
method public suspend <R> Object? handlePointerInput(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.HandlePointerInputScope,? super kotlin.coroutines.Continuation<? super R>,?> handler, kotlin.coroutines.Continuation<? super R> p);
- property public abstract androidx.compose.ui.input.pointer.CustomEventDispatcher customEventDispatcher;
property public abstract long size;
property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
}
@@ -1626,7 +1674,7 @@
}
public final class SuspendingPointerInputFilterKt {
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
}
}
@@ -1739,6 +1787,22 @@
property public abstract Object layoutId;
}
+ public interface LayoutInfo {
+ method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
+ method public int getHeight();
+ method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
+ method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
+ method public int getWidth();
+ method public boolean isAttached();
+ method public boolean isPlaced();
+ property public abstract androidx.compose.ui.layout.LayoutCoordinates coordinates;
+ property public abstract int height;
+ property public abstract boolean isAttached;
+ property public abstract boolean isPlaced;
+ property public abstract androidx.compose.ui.layout.LayoutInfo? parentInfo;
+ property public abstract int width;
+ }
+
public final class LayoutKt {
method @androidx.compose.runtime.Composable public static void Layout(kotlin.jvm.functions.Function0<kotlin.Unit> content, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicHeightMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicHeightMeasureBlock, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
method @androidx.compose.runtime.Composable public static void Layout(kotlin.jvm.functions.Function0<kotlin.Unit> content, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
@@ -1794,6 +1858,16 @@
method public static inline String! toString-impl(androidx.compose.ui.layout.Placeable! p);
}
+ public final class ModifierInfo {
+ ctor public ModifierInfo(androidx.compose.ui.Modifier modifier, androidx.compose.ui.layout.LayoutCoordinates coordinates, Object? extra);
+ method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
+ method public Object? getExtra();
+ method public androidx.compose.ui.Modifier getModifier();
+ property public final androidx.compose.ui.layout.LayoutCoordinates coordinates;
+ property public final Object? extra;
+ property public final androidx.compose.ui.Modifier modifier;
+ }
+
public interface OnGloballyPositionedModifier extends androidx.compose.ui.Modifier.Element {
method public void onGloballyPositioned(androidx.compose.ui.layout.LayoutCoordinates coordinates);
}
@@ -1899,6 +1973,9 @@
method public java.util.List<androidx.compose.ui.layout.Measurable> subcompose(Object? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
+ public final class TestModifierUpdaterKt {
+ }
+
public final class VerticalAlignmentLine extends androidx.compose.ui.layout.AlignmentLine {
ctor public VerticalAlignmentLine(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Integer> merger);
}
@@ -1923,25 +2000,15 @@
@kotlin.RequiresOptIn(message="This API is internal to library.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface InternalCoreApi {
}
- public final class LayoutNode implements androidx.compose.ui.layout.Measurable androidx.compose.ui.node.OwnerScope androidx.compose.ui.layout.Remeasurement {
- ctor public LayoutNode();
- method public void attach(androidx.compose.ui.node.Owner owner);
- method public void detach();
+ public final class LayoutNode implements androidx.compose.ui.layout.LayoutInfo androidx.compose.ui.layout.Measurable androidx.compose.ui.node.OwnerScope androidx.compose.ui.layout.Remeasurement {
method public void forceRemeasure();
- method @Deprecated public boolean getCanMultiMeasure();
- method public java.util.List<androidx.compose.ui.node.LayoutNode> getChildren();
method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
- method public androidx.compose.ui.unit.Density getDensity();
- method public int getDepth();
method public int getHeight();
- method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
- method public androidx.compose.ui.node.MeasureBlocks getMeasureBlocks();
- method public androidx.compose.ui.Modifier getModifier();
- method public java.util.List<androidx.compose.ui.node.ModifierInfo> getModifierInfo();
- method public androidx.compose.ui.node.Owner? getOwner();
- method public androidx.compose.ui.node.LayoutNode? getParent();
+ method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
method public Object? getParentData();
+ method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
method public int getWidth();
+ method public boolean isAttached();
method public boolean isPlaced();
method public boolean isValid();
method public int maxIntrinsicHeight(int width);
@@ -1949,28 +2016,14 @@
method public androidx.compose.ui.layout.Placeable measure-BRTryo0(long constraints);
method public int minIntrinsicHeight(int width);
method public int minIntrinsicWidth(int height);
- method public void place(int x, int y);
- method @Deprecated public void setCanMultiMeasure(boolean p);
- method public void setDensity(androidx.compose.ui.unit.Density p);
- method public void setDepth(int p);
- method public void setLayoutDirection(androidx.compose.ui.unit.LayoutDirection value);
- method public void setMeasureBlocks(androidx.compose.ui.node.MeasureBlocks value);
- method public void setModifier(androidx.compose.ui.Modifier value);
- property @Deprecated public final boolean canMultiMeasure;
- property public final java.util.List<androidx.compose.ui.node.LayoutNode> children;
- property public final androidx.compose.ui.layout.LayoutCoordinates coordinates;
- property public final androidx.compose.ui.unit.Density density;
- property public final int depth;
- property public final int height;
- property public final boolean isPlaced;
+ property public androidx.compose.ui.layout.LayoutCoordinates coordinates;
+ property public int height;
+ property public boolean isAttached;
+ property public boolean isPlaced;
property public boolean isValid;
- property public final androidx.compose.ui.unit.LayoutDirection layoutDirection;
- property public final androidx.compose.ui.node.MeasureBlocks measureBlocks;
- property public final androidx.compose.ui.Modifier modifier;
- property public final androidx.compose.ui.node.Owner? owner;
- property public final androidx.compose.ui.node.LayoutNode? parent;
property public Object? parentData;
- property public final int width;
+ property public androidx.compose.ui.layout.LayoutInfo? parentInfo;
+ property public int width;
}
public final class LayoutNodeKt {
@@ -1984,16 +2037,6 @@
method public int minIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int h);
}
- public final class ModifierInfo {
- ctor public ModifierInfo(androidx.compose.ui.Modifier modifier, androidx.compose.ui.layout.LayoutCoordinates coordinates, Object? extra);
- method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
- method public Object? getExtra();
- method public androidx.compose.ui.Modifier getModifier();
- property public final androidx.compose.ui.layout.LayoutCoordinates coordinates;
- property public final Object? extra;
- property public final androidx.compose.ui.Modifier modifier;
- }
-
public interface OwnedLayer {
method public void destroy();
method public void drawLayer(androidx.compose.ui.graphics.Canvas canvas);
@@ -2017,7 +2060,6 @@
method public androidx.compose.ui.focus.FocusManager getFocusManager();
method public androidx.compose.ui.text.font.Font.ResourceLoader getFontLoader();
method public androidx.compose.ui.hapticfeedback.HapticFeedback getHapticFeedBack();
- method public boolean getHasPendingMeasureOrLayout();
method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
method public long getMeasureIteration();
method public androidx.compose.ui.node.LayoutNode getRoot();
@@ -2035,7 +2077,7 @@
method public void onRequestRelayout(androidx.compose.ui.node.LayoutNode layoutNode);
method public void onSemanticsChange();
method public boolean requestFocus();
- method @androidx.compose.ui.input.key.ExperimentalKeyInput public boolean sendKeyEvent(androidx.compose.ui.input.key.KeyEvent keyEvent);
+ method public boolean sendKeyEvent(androidx.compose.ui.input.key.KeyEvent keyEvent);
property public abstract androidx.compose.ui.autofill.Autofill? autofill;
property public abstract androidx.compose.ui.autofill.AutofillTree autofillTree;
property public abstract androidx.compose.ui.platform.ClipboardManager clipboardManager;
@@ -2043,7 +2085,6 @@
property public abstract androidx.compose.ui.focus.FocusManager focusManager;
property public abstract androidx.compose.ui.text.font.Font.ResourceLoader fontLoader;
property public abstract androidx.compose.ui.hapticfeedback.HapticFeedback hapticFeedBack;
- property public abstract boolean hasPendingMeasureOrLayout;
property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
property public abstract long measureIteration;
property public abstract androidx.compose.ui.node.LayoutNode root;
@@ -2079,16 +2120,13 @@
property public final T? value;
}
- public final class UiApplier implements androidx.compose.runtime.Applier<java.lang.Object> {
+ public final class UiApplier extends androidx.compose.runtime.AbstractApplier<java.lang.Object> {
ctor public UiApplier(Object root);
- method public void clear();
- method public void down(Object node);
- method public Object getCurrent();
- method public void insert(int index, Object instance);
+ method public void insertBottomUp(int index, Object instance);
+ method public void insertTopDown(int index, Object instance);
method public void move(int from, int to, int count);
+ method protected void onClear();
method public void remove(int index, int count);
- method public void up();
- property public Object current;
}
public final class ViewInteropKt {
@@ -2126,8 +2164,6 @@
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ViewConfiguration> getAmbientViewConfiguration();
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowManager> getAmbientWindowManager();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.animation.core.AnimationClockObservable>! getAnimationClockAmbient();
- method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.Autofill>! getAutofillAmbient();
- method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.AutofillTree>! getAutofillTreeAmbient();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ClipboardManager>! getClipboardManagerAmbient();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.unit.Density>! getDensityAmbient();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.focus.FocusManager>! getFocusManagerAmbient();
@@ -2159,31 +2195,6 @@
public final class AndroidComposeViewKt {
}
- public interface AndroidOwner extends androidx.compose.ui.node.Owner {
- method public void addAndroidView(androidx.compose.ui.viewinterop.AndroidViewHolder view, androidx.compose.ui.node.LayoutNode layoutNode);
- method public void drawAndroidView(androidx.compose.ui.viewinterop.AndroidViewHolder view, android.graphics.Canvas canvas);
- method public kotlin.jvm.functions.Function1<android.content.res.Configuration,kotlin.Unit> getConfigurationChangeObserver();
- method public android.view.View getView();
- method public androidx.compose.ui.platform.AndroidOwner.ViewTreeOwners? getViewTreeOwners();
- method public void invalidateDescendants();
- method public void removeAndroidView(androidx.compose.ui.viewinterop.AndroidViewHolder view);
- method public void setConfigurationChangeObserver(kotlin.jvm.functions.Function1<? super android.content.res.Configuration,kotlin.Unit> p);
- method public void setOnViewTreeOwnersAvailable(kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.AndroidOwner.ViewTreeOwners,kotlin.Unit> callback);
- property public abstract kotlin.jvm.functions.Function1<android.content.res.Configuration,kotlin.Unit> configurationChangeObserver;
- property public abstract android.view.View view;
- property public abstract androidx.compose.ui.platform.AndroidOwner.ViewTreeOwners? viewTreeOwners;
- }
-
- public static final class AndroidOwner.ViewTreeOwners {
- ctor public AndroidOwner.ViewTreeOwners(androidx.lifecycle.LifecycleOwner lifecycleOwner, androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, androidx.savedstate.SavedStateRegistryOwner savedStateRegistryOwner);
- method public androidx.lifecycle.LifecycleOwner getLifecycleOwner();
- method public androidx.savedstate.SavedStateRegistryOwner getSavedStateRegistryOwner();
- method public androidx.lifecycle.ViewModelStoreOwner getViewModelStoreOwner();
- property public final androidx.lifecycle.LifecycleOwner lifecycleOwner;
- property public final androidx.savedstate.SavedStateRegistryOwner savedStateRegistryOwner;
- property public final androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner;
- }
-
public final class AndroidUriHandler implements androidx.compose.ui.platform.UriHandler {
ctor public AndroidUriHandler(android.content.Context context);
method public void openUri(String uri);
@@ -2262,10 +2273,6 @@
public final class JvmActualsKt {
}
- public final class SubcompositionKt {
- method @MainThread public static androidx.compose.runtime.Composition subcomposeInto(androidx.compose.ui.node.LayoutNode container, androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> composable);
- }
-
public final class TestTagKt {
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier testTag(androidx.compose.ui.Modifier, String tag);
}
@@ -2317,6 +2324,23 @@
property public abstract float touchSlop;
}
+ @VisibleForTesting public interface ViewRootForTest extends androidx.compose.ui.node.Owner {
+ method public boolean getHasPendingMeasureOrLayout();
+ method public android.view.View getView();
+ method public void invalidateDescendants();
+ method public boolean isLifecycleInResumedState();
+ property public abstract boolean hasPendingMeasureOrLayout;
+ property public abstract boolean isLifecycleInResumedState;
+ property public abstract android.view.View view;
+ field public static final androidx.compose.ui.platform.ViewRootForTest.Companion Companion;
+ }
+
+ public static final class ViewRootForTest.Companion {
+ method public kotlin.jvm.functions.Function1<androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? getOnViewCreatedCallback();
+ method public void setOnViewCreatedCallback(kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? p);
+ property public final kotlin.jvm.functions.Function1<androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? onViewCreatedCallback;
+ }
+
@androidx.compose.runtime.Stable public interface WindowManager {
method public boolean isWindowFocused();
property public abstract boolean isWindowFocused;
@@ -2412,7 +2436,7 @@
package androidx.compose.ui.selection {
- public interface Selectable {
+ @androidx.compose.ui.text.ExperimentalTextApi public interface Selectable {
method public androidx.compose.ui.geometry.Rect getBoundingBox(int offset);
method public long getHandlePosition-F1C5BW0(androidx.compose.ui.selection.Selection selection, boolean isStartHandle);
method public androidx.compose.ui.layout.LayoutCoordinates? getLayoutCoordinates();
@@ -2462,9 +2486,11 @@
public final class SelectionManagerKt {
}
- public interface SelectionRegistrar {
- method public void onPositionChange();
- method public void onUpdateSelection-rULFVbc(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition, long endPosition);
+ @androidx.compose.ui.text.ExperimentalTextApi public interface SelectionRegistrar {
+ method public void notifyPositionChange();
+ method public void notifySelectionUpdate-rULFVbc(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition, long endPosition);
+ method public void notifySelectionUpdateEnd();
+ method public void notifySelectionUpdateStart-YJiYy8w(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition);
method public androidx.compose.ui.selection.Selectable subscribe(androidx.compose.ui.selection.Selectable selectable);
method public void unsubscribe(androidx.compose.ui.selection.Selectable selectable);
}
@@ -2612,8 +2638,9 @@
method public androidx.compose.ui.geometry.Rect getGlobalBounds();
method public long getGlobalPosition-F1C5BW0();
method public int getId();
- method public androidx.compose.ui.node.LayoutNode getLayoutNode();
+ method public androidx.compose.ui.layout.LayoutInfo getLayoutInfo();
method public boolean getMergingEnabled();
+ method public androidx.compose.ui.node.Owner? getOwner();
method public androidx.compose.ui.semantics.SemanticsNode? getParent();
method public long getPositionInRoot-F1C5BW0();
method public long getSize-YbymL2g();
@@ -2625,8 +2652,9 @@
property public final long globalPosition;
property public final int id;
property public final boolean isRoot;
- property public final androidx.compose.ui.node.LayoutNode layoutNode;
+ property public final androidx.compose.ui.layout.LayoutInfo layoutInfo;
property public final boolean mergingEnabled;
+ property public final androidx.compose.ui.node.Owner? owner;
property public final androidx.compose.ui.semantics.SemanticsNode? parent;
property public final long positionInRoot;
property public final long size;
@@ -2649,9 +2677,8 @@
}
public final class SemanticsProperties {
- method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getAccessibilityLabel();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityRangeInfo> getAccessibilityRangeInfo();
- method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getAccessibilityValue();
+ method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getContentDescription();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getDisabled();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> getFocused();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getHidden();
@@ -2660,14 +2687,14 @@
method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getIsDialog();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getIsPopup();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> getSelected();
+ method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getStateDescription();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getTestTag();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.AnnotatedString> getText();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.TextRange> getTextSelectionRange();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.state.ToggleableState> getToggleableState();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityScrollState> getVerticalAccessibilityScrollState();
- property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> AccessibilityLabel;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityRangeInfo> AccessibilityRangeInfo;
- property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> AccessibilityValue;
+ property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> ContentDescription;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> Disabled;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> Focused;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> Hidden;
@@ -2676,6 +2703,7 @@
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> IsDialog;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> IsPopup;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> Selected;
+ property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> StateDescription;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> TestTag;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.AnnotatedString> Text;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.TextRange> TextSelectionRange;
@@ -2690,14 +2718,16 @@
method public static void dialog(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void disabled(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void dismiss(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
- method public static String getAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
- method public static String getAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
- method public static androidx.compose.ui.semantics.AccessibilityRangeInfo getAccessibilityValueRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method @Deprecated public static String getAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method @Deprecated public static String getAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method public static String getContentDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction> getCustomActions(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static boolean getFocused(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static androidx.compose.ui.semantics.AccessibilityScrollState getHorizontalAccessibilityScrollState(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static androidx.compose.ui.text.input.ImeAction getImeAction(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static boolean getSelected(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method public static String getStateDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method public static androidx.compose.ui.semantics.AccessibilityRangeInfo getStateDescriptionRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static String getTestTag(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static androidx.compose.ui.text.AnnotatedString getText(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void getTextLayoutResult(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.text.TextLayoutResult>,java.lang.Boolean> action);
@@ -2710,9 +2740,9 @@
method public static void pasteText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
method public static void popup(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void scrollBy(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> action);
- method public static void setAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
- method public static void setAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
- method public static void setAccessibilityValueRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.semantics.AccessibilityRangeInfo p);
+ method @Deprecated public static void setAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
+ method @Deprecated public static void setAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
+ method public static void setContentDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
method public static void setCustomActions(androidx.compose.ui.semantics.SemanticsPropertyReceiver, java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction> p);
method public static void setFocused(androidx.compose.ui.semantics.SemanticsPropertyReceiver, boolean p);
method public static void setHorizontalAccessibilityScrollState(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.semantics.AccessibilityScrollState p);
@@ -2720,6 +2750,8 @@
method public static void setProgress(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> action);
method public static void setSelected(androidx.compose.ui.semantics.SemanticsPropertyReceiver, boolean p);
method public static void setSelection(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super java.lang.Integer,? super java.lang.Boolean,java.lang.Boolean> action);
+ method public static void setStateDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
+ method public static void setStateDescriptionRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.semantics.AccessibilityRangeInfo p);
method public static void setTestTag(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
method public static void setText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.text.AnnotatedString p);
method public static void setText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString,java.lang.Boolean> action);
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 5461ac24..1cffa60 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -132,26 +132,29 @@
method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
}
- public final class FocusModifierKt {
- method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focus(androidx.compose.ui.Modifier);
+ @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalComposeUiApi {
}
- @androidx.compose.ui.focus.ExperimentalFocus public interface FocusObserverModifier extends androidx.compose.ui.Modifier.Element {
+ public final class FocusModifierKt {
+ method public static androidx.compose.ui.Modifier focus(androidx.compose.ui.Modifier);
+ }
+
+ public interface FocusObserverModifier extends androidx.compose.ui.Modifier.Element {
method public kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusState,kotlin.Unit> getOnFocusChange();
property public abstract kotlin.jvm.functions.Function1<androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChange;
}
public final class FocusObserverModifierKt {
- method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focusObserver(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChange);
+ method public static androidx.compose.ui.Modifier focusObserver(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusChange);
}
- @androidx.compose.ui.focus.ExperimentalFocus public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
+ public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
method public androidx.compose.ui.focus.FocusRequester getFocusRequester();
property public abstract androidx.compose.ui.focus.FocusRequester focusRequester;
}
public final class FocusRequesterModifierKt {
- method @androidx.compose.ui.focus.ExperimentalFocus public static androidx.compose.ui.Modifier focusRequester(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester);
+ method public static androidx.compose.ui.Modifier focusRequester(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester);
}
@androidx.compose.runtime.Stable public interface Modifier {
@@ -194,17 +197,17 @@
public final class AndroidAutofillTypeKt {
}
- public interface Autofill {
+ @androidx.compose.ui.ExperimentalComposeUiApi public interface Autofill {
method public void cancelAutofillForNode(androidx.compose.ui.autofill.AutofillNode autofillNode);
method public void requestAutofillForNode(androidx.compose.ui.autofill.AutofillNode autofillNode);
}
- public final class AutofillNode {
+ @androidx.compose.ui.ExperimentalComposeUiApi public final class AutofillNode {
ctor public AutofillNode(java.util.List<? extends androidx.compose.ui.autofill.AutofillType> autofillTypes, androidx.compose.ui.geometry.Rect? boundingBox, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit>? onFill);
method public java.util.List<androidx.compose.ui.autofill.AutofillType> component1();
method public androidx.compose.ui.geometry.Rect? component2();
method public kotlin.jvm.functions.Function1<java.lang.String,kotlin.Unit>? component3();
- method public androidx.compose.ui.autofill.AutofillNode copy(java.util.List<? extends androidx.compose.ui.autofill.AutofillType> autofillTypes, androidx.compose.ui.geometry.Rect? boundingBox, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit>? onFill);
+ method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.autofill.AutofillNode copy(java.util.List<? extends androidx.compose.ui.autofill.AutofillType> autofillTypes, androidx.compose.ui.geometry.Rect? boundingBox, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit>? onFill);
method public java.util.List<androidx.compose.ui.autofill.AutofillType> getAutofillTypes();
method public androidx.compose.ui.geometry.Rect? getBoundingBox();
method public int getId();
@@ -216,7 +219,7 @@
property public final kotlin.jvm.functions.Function1<java.lang.String,kotlin.Unit>? onFill;
}
- public final class AutofillTree {
+ @androidx.compose.ui.ExperimentalComposeUiApi public final class AutofillTree {
ctor public AutofillTree();
method public java.util.Map<java.lang.Integer,androidx.compose.ui.autofill.AutofillNode> getChildren();
method public kotlin.Unit? performAutofill(int id, String value);
@@ -224,7 +227,7 @@
property public final java.util.Map<java.lang.Integer,androidx.compose.ui.autofill.AutofillNode> children;
}
- public enum AutofillType {
+ @androidx.compose.ui.ExperimentalComposeUiApi public enum AutofillType {
enum_constant public static final androidx.compose.ui.autofill.AutofillType AddressAuxiliaryDetails;
enum_constant public static final androidx.compose.ui.autofill.AutofillType AddressCountry;
enum_constant public static final androidx.compose.ui.autofill.AutofillType AddressLocality;
@@ -328,27 +331,49 @@
package androidx.compose.ui.focus {
- @kotlin.RequiresOptIn(message="The Focus API is experimental and is likely to change in the future.") public @interface ExperimentalFocus {
- }
-
- @androidx.compose.ui.focus.ExperimentalFocus public interface FocusManager {
+ public interface FocusManager {
method public void clearFocus(optional boolean forcedClear);
}
public final class FocusNodeUtilsKt {
}
- @androidx.compose.ui.focus.ExperimentalFocus public final class FocusRequester {
+ public final class FocusRequester {
ctor public FocusRequester();
method public boolean captureFocus();
method public boolean freeFocus();
method public void requestFocus();
+ field public static final androidx.compose.ui.focus.FocusRequester.Companion Companion;
+ }
+
+ public static final class FocusRequester.Companion {
+ method public androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory createRefs();
+ }
+
+ public static final class FocusRequester.Companion.FocusRequesterFactory {
+ method public operator androidx.compose.ui.focus.FocusRequester component1();
+ method public operator androidx.compose.ui.focus.FocusRequester component10();
+ method public operator androidx.compose.ui.focus.FocusRequester component11();
+ method public operator androidx.compose.ui.focus.FocusRequester component12();
+ method public operator androidx.compose.ui.focus.FocusRequester component13();
+ method public operator androidx.compose.ui.focus.FocusRequester component14();
+ method public operator androidx.compose.ui.focus.FocusRequester component15();
+ method public operator androidx.compose.ui.focus.FocusRequester component16();
+ method public operator androidx.compose.ui.focus.FocusRequester component2();
+ method public operator androidx.compose.ui.focus.FocusRequester component3();
+ method public operator androidx.compose.ui.focus.FocusRequester component4();
+ method public operator androidx.compose.ui.focus.FocusRequester component5();
+ method public operator androidx.compose.ui.focus.FocusRequester component6();
+ method public operator androidx.compose.ui.focus.FocusRequester component7();
+ method public operator androidx.compose.ui.focus.FocusRequester component8();
+ method public operator androidx.compose.ui.focus.FocusRequester component9();
+ field public static final androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory INSTANCE;
}
public final class FocusRequesterKt {
}
- @androidx.compose.ui.focus.ExperimentalFocus public enum FocusState {
+ public enum FocusState {
enum_constant public static final androidx.compose.ui.focus.FocusState Active;
enum_constant public static final androidx.compose.ui.focus.FocusState ActiveParent;
enum_constant public static final androidx.compose.ui.focus.FocusState Captured;
@@ -410,9 +435,6 @@
method public static androidx.compose.ui.Modifier dragSlopExceededGestureFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function0<kotlin.Unit> onDragSlopExceeded, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.gesture.Direction,java.lang.Boolean>? canDrag, optional androidx.compose.ui.gesture.scrollorientationlocking.Orientation? orientation);
}
- @kotlin.RequiresOptIn(message="This pointer input API is experimental and is likely to change before becoming " + "stable.") public @interface ExperimentalPointerInput {
- }
-
public final class GestureUtilsKt {
method public static boolean anyPointersInBounds-5eFHUEc(java.util.List<androidx.compose.ui.input.pointer.PointerInputChange>, long bounds);
}
@@ -493,11 +515,11 @@
package androidx.compose.ui.gesture.customevents {
- @androidx.compose.ui.gesture.ExperimentalPointerInput public final class DelayUpEvent implements androidx.compose.ui.input.pointer.CustomEvent {
+ public final class DelayUpEvent implements androidx.compose.ui.input.pointer.CustomEvent {
ctor public DelayUpEvent(androidx.compose.ui.gesture.customevents.DelayUpMessage message, java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers);
method public androidx.compose.ui.gesture.customevents.DelayUpMessage component1();
method public java.util.Set<androidx.compose.ui.input.pointer.PointerId> component2();
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public androidx.compose.ui.gesture.customevents.DelayUpEvent copy(androidx.compose.ui.gesture.customevents.DelayUpMessage message, java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers);
+ method public androidx.compose.ui.gesture.customevents.DelayUpEvent copy(androidx.compose.ui.gesture.customevents.DelayUpMessage message, java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers);
method public androidx.compose.ui.gesture.customevents.DelayUpMessage getMessage();
method public java.util.Set<androidx.compose.ui.input.pointer.PointerId> getPointers();
method public void setMessage(androidx.compose.ui.gesture.customevents.DelayUpMessage p);
@@ -505,7 +527,7 @@
property public final java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointers;
}
- @androidx.compose.ui.gesture.ExperimentalPointerInput public enum DelayUpMessage {
+ public enum DelayUpMessage {
enum_constant public static final androidx.compose.ui.gesture.customevents.DelayUpMessage DelayUp;
enum_constant public static final androidx.compose.ui.gesture.customevents.DelayUpMessage DelayedUpConsumed;
enum_constant public static final androidx.compose.ui.gesture.customevents.DelayUpMessage DelayedUpNotConsumed;
@@ -517,6 +539,37 @@
}
+package androidx.compose.ui.gesture.nestedscroll {
+
+ public interface NestedScrollConnection {
+ method public default void onPostFling-Pv53iXo(long consumed, long available, kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Velocity,kotlin.Unit> onFinished);
+ method public default long onPostScroll-l-UAZDg(long consumed, long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+ method public default long onPreFling-TH1AsA0(long available);
+ method public default long onPreScroll-vG6bCaM(long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+ }
+
+ public final class NestedScrollDelegatingWrapperKt {
+ }
+
+ public final class NestedScrollDispatcher {
+ ctor public NestedScrollDispatcher();
+ method public void dispatchPostFling-uYzo7IE(long consumed, long available);
+ method public long dispatchPostScroll-l-UAZDg(long consumed, long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+ method public long dispatchPreFling-TH1AsA0(long available);
+ method public long dispatchPreScroll-vG6bCaM(long available, androidx.compose.ui.gesture.nestedscroll.NestedScrollSource source);
+ }
+
+ public final class NestedScrollModifierKt {
+ method public static androidx.compose.ui.Modifier nestedScroll(androidx.compose.ui.Modifier, androidx.compose.ui.gesture.nestedscroll.NestedScrollConnection connection, optional androidx.compose.ui.gesture.nestedscroll.NestedScrollDispatcher? dispatcher);
+ }
+
+ public enum NestedScrollSource {
+ enum_constant public static final androidx.compose.ui.gesture.nestedscroll.NestedScrollSource Drag;
+ enum_constant public static final androidx.compose.ui.gesture.nestedscroll.NestedScrollSource Fling;
+ }
+
+}
+
package androidx.compose.ui.gesture.scrollorientationlocking {
public enum Orientation {
@@ -524,7 +577,7 @@
enum_constant public static final androidx.compose.ui.gesture.scrollorientationlocking.Orientation Vertical;
}
- @androidx.compose.ui.gesture.ExperimentalPointerInput public final class ScrollOrientationLocker {
+ public final class ScrollOrientationLocker {
ctor public ScrollOrientationLocker(androidx.compose.ui.input.pointer.CustomEventDispatcher customEventDispatcher);
method public void attemptToLockPointers(java.util.List<androidx.compose.ui.input.pointer.PointerInputChange> changes, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation);
method public java.util.List<androidx.compose.ui.input.pointer.PointerInputChange> getPointersFor(java.util.List<androidx.compose.ui.input.pointer.PointerInputChange> changes, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation);
@@ -678,7 +731,8 @@
public final class VectorApplier extends androidx.compose.runtime.AbstractApplier<androidx.compose.ui.graphics.vector.VNode> {
ctor public VectorApplier(androidx.compose.ui.graphics.vector.VNode root);
- method public void insert(int index, androidx.compose.ui.graphics.vector.VNode instance);
+ method public void insertBottomUp(int index, androidx.compose.ui.graphics.vector.VNode instance);
+ method public void insertTopDown(int index, androidx.compose.ui.graphics.vector.VNode instance);
method public void move(int from, int to, int count);
method protected void onClear();
method public void remove(int index, int count);
@@ -811,7 +865,7 @@
package androidx.compose.ui.input.key {
- @Deprecated @androidx.compose.ui.input.key.ExperimentalKeyInput public interface Alt {
+ @Deprecated public interface Alt {
method @Deprecated public boolean isLeftAltPressed();
method @Deprecated public default boolean isPressed();
method @Deprecated public boolean isRightAltPressed();
@@ -820,9 +874,6 @@
property public abstract boolean isRightAltPressed;
}
- @kotlin.RequiresOptIn(message="The Key Input API is experimental and is likely to change in the future.") public @interface ExperimentalKeyInput {
- }
-
public final inline class Key {
ctor public Key();
method public static int constructor-impl(int keyCode);
@@ -1416,7 +1467,7 @@
property public final int ZoomOut;
}
- @androidx.compose.ui.input.key.ExperimentalKeyInput public interface KeyEvent {
+ public interface KeyEvent {
method @Deprecated public androidx.compose.ui.input.key.Alt getAlt();
method public int getKey-EK5gGoQ();
method public androidx.compose.ui.input.key.KeyEventType getType();
@@ -1435,15 +1486,15 @@
property public abstract int utf16CodePoint;
}
- @androidx.compose.ui.input.key.ExperimentalKeyInput public enum KeyEventType {
+ public enum KeyEventType {
enum_constant public static final androidx.compose.ui.input.key.KeyEventType KeyDown;
enum_constant public static final androidx.compose.ui.input.key.KeyEventType KeyUp;
enum_constant public static final androidx.compose.ui.input.key.KeyEventType Unknown;
}
public final class KeyInputModifierKt {
- method @androidx.compose.ui.input.key.ExperimentalKeyInput public static androidx.compose.ui.Modifier keyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onKeyEvent);
- method @androidx.compose.ui.input.key.ExperimentalKeyInput public static androidx.compose.ui.Modifier previewKeyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
+ method public static androidx.compose.ui.Modifier keyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onKeyEvent);
+ method public static androidx.compose.ui.Modifier previewKeyInputFilter(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
}
}
@@ -1472,8 +1523,7 @@
method public void retainHitPaths(java.util.Set<androidx.compose.ui.input.pointer.PointerId> pointerIds);
}
- @androidx.compose.ui.gesture.ExperimentalPointerInput @kotlin.coroutines.RestrictsSuspension public interface HandlePointerInputScope extends androidx.compose.ui.unit.Density {
- method public suspend Object? awaitCustomEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.CustomEvent> p);
+ @kotlin.coroutines.RestrictsSuspension public interface HandlePointerInputScope extends androidx.compose.ui.unit.Density {
method public suspend Object? awaitPointerEvent(optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerEvent> p);
method public androidx.compose.ui.input.pointer.PointerEvent getCurrentEvent();
method public long getSize-YbymL2g();
@@ -1600,12 +1650,10 @@
property public abstract androidx.compose.ui.input.pointer.PointerInputFilter pointerInputFilter;
}
- @androidx.compose.ui.gesture.ExperimentalPointerInput public interface PointerInputScope extends androidx.compose.ui.unit.Density {
- method public androidx.compose.ui.input.pointer.CustomEventDispatcher getCustomEventDispatcher();
+ public interface PointerInputScope extends androidx.compose.ui.unit.Density {
method public long getSize-YbymL2g();
method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
method public suspend <R> Object? handlePointerInput(kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.HandlePointerInputScope,? super kotlin.coroutines.Continuation<? super R>,?> handler, kotlin.coroutines.Continuation<? super R> p);
- property public abstract androidx.compose.ui.input.pointer.CustomEventDispatcher customEventDispatcher;
property public abstract long size;
property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
}
@@ -1626,7 +1674,7 @@
}
public final class SuspendingPointerInputFilterKt {
- method @androidx.compose.ui.gesture.ExperimentalPointerInput public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
+ method public static androidx.compose.ui.Modifier pointerInput(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.input.pointer.PointerInputScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block);
}
}
@@ -1785,6 +1833,22 @@
property public abstract Object layoutId;
}
+ public interface LayoutInfo {
+ method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
+ method public int getHeight();
+ method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
+ method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
+ method public int getWidth();
+ method public boolean isAttached();
+ method public boolean isPlaced();
+ property public abstract androidx.compose.ui.layout.LayoutCoordinates coordinates;
+ property public abstract int height;
+ property public abstract boolean isAttached;
+ property public abstract boolean isPlaced;
+ property public abstract androidx.compose.ui.layout.LayoutInfo? parentInfo;
+ property public abstract int width;
+ }
+
public final class LayoutKt {
method @androidx.compose.runtime.Composable public static void Layout(kotlin.jvm.functions.Function0<kotlin.Unit> content, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> minIntrinsicHeightMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicWidthMeasureBlock, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.IntrinsicMeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable>,? super java.lang.Integer,java.lang.Integer> maxIntrinsicHeightMeasureBlock, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
method @androidx.compose.runtime.Composable public static void Layout(kotlin.jvm.functions.Function0<kotlin.Unit> content, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function3<? super androidx.compose.ui.layout.MeasureScope,? super java.util.List<? extends androidx.compose.ui.layout.Measurable>,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measureBlock);
@@ -1841,6 +1905,16 @@
method public static inline String! toString-impl(androidx.compose.ui.layout.Placeable! p);
}
+ public final class ModifierInfo {
+ ctor public ModifierInfo(androidx.compose.ui.Modifier modifier, androidx.compose.ui.layout.LayoutCoordinates coordinates, Object? extra);
+ method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
+ method public Object? getExtra();
+ method public androidx.compose.ui.Modifier getModifier();
+ property public final androidx.compose.ui.layout.LayoutCoordinates coordinates;
+ property public final Object? extra;
+ property public final androidx.compose.ui.Modifier modifier;
+ }
+
public interface OnGloballyPositionedModifier extends androidx.compose.ui.Modifier.Element {
method public void onGloballyPositioned(androidx.compose.ui.layout.LayoutCoordinates coordinates);
}
@@ -1946,6 +2020,9 @@
method public java.util.List<androidx.compose.ui.layout.Measurable> subcompose(Object? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
+ public final class TestModifierUpdaterKt {
+ }
+
public final class VerticalAlignmentLine extends androidx.compose.ui.layout.AlignmentLine {
ctor public VerticalAlignmentLine(kotlin.jvm.functions.Function2<? super java.lang.Integer,? super java.lang.Integer,java.lang.Integer> merger);
}
@@ -1985,25 +2062,15 @@
property public final kotlin.jvm.functions.Function2<androidx.compose.ui.node.LayoutNode,androidx.compose.ui.node.Ref<androidx.compose.ui.node.LayoutNode>,kotlin.Unit> setRef;
}
- public final class LayoutNode implements androidx.compose.ui.layout.Measurable androidx.compose.ui.node.OwnerScope androidx.compose.ui.layout.Remeasurement {
- ctor public LayoutNode();
- method public void attach(androidx.compose.ui.node.Owner owner);
- method public void detach();
+ public final class LayoutNode implements androidx.compose.ui.layout.LayoutInfo androidx.compose.ui.layout.Measurable androidx.compose.ui.node.OwnerScope androidx.compose.ui.layout.Remeasurement {
method public void forceRemeasure();
- method @Deprecated public boolean getCanMultiMeasure();
- method public java.util.List<androidx.compose.ui.node.LayoutNode> getChildren();
method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
- method public androidx.compose.ui.unit.Density getDensity();
- method public int getDepth();
method public int getHeight();
- method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
- method public androidx.compose.ui.node.MeasureBlocks getMeasureBlocks();
- method public androidx.compose.ui.Modifier getModifier();
- method public java.util.List<androidx.compose.ui.node.ModifierInfo> getModifierInfo();
- method public androidx.compose.ui.node.Owner? getOwner();
- method public androidx.compose.ui.node.LayoutNode? getParent();
+ method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
method public Object? getParentData();
+ method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
method public int getWidth();
+ method public boolean isAttached();
method public boolean isPlaced();
method public boolean isValid();
method public int maxIntrinsicHeight(int width);
@@ -2011,28 +2078,14 @@
method public androidx.compose.ui.layout.Placeable measure-BRTryo0(long constraints);
method public int minIntrinsicHeight(int width);
method public int minIntrinsicWidth(int height);
- method public void place(int x, int y);
- method @Deprecated public void setCanMultiMeasure(boolean p);
- method public void setDensity(androidx.compose.ui.unit.Density p);
- method public void setDepth(int p);
- method public void setLayoutDirection(androidx.compose.ui.unit.LayoutDirection value);
- method public void setMeasureBlocks(androidx.compose.ui.node.MeasureBlocks value);
- method public void setModifier(androidx.compose.ui.Modifier value);
- property @Deprecated public final boolean canMultiMeasure;
- property public final java.util.List<androidx.compose.ui.node.LayoutNode> children;
- property public final androidx.compose.ui.layout.LayoutCoordinates coordinates;
- property public final androidx.compose.ui.unit.Density density;
- property public final int depth;
- property public final int height;
- property public final boolean isPlaced;
+ property public androidx.compose.ui.layout.LayoutCoordinates coordinates;
+ property public int height;
+ property public boolean isAttached;
+ property public boolean isPlaced;
property public boolean isValid;
- property public final androidx.compose.ui.unit.LayoutDirection layoutDirection;
- property public final androidx.compose.ui.node.MeasureBlocks measureBlocks;
- property public final androidx.compose.ui.Modifier modifier;
- property public final androidx.compose.ui.node.Owner? owner;
- property public final androidx.compose.ui.node.LayoutNode? parent;
property public Object? parentData;
- property public final int width;
+ property public androidx.compose.ui.layout.LayoutInfo? parentInfo;
+ property public int width;
}
public final class LayoutNodeKt {
@@ -2046,16 +2099,6 @@
method public int minIntrinsicWidth(androidx.compose.ui.layout.IntrinsicMeasureScope intrinsicMeasureScope, java.util.List<? extends androidx.compose.ui.layout.IntrinsicMeasurable> measurables, int h);
}
- public final class ModifierInfo {
- ctor public ModifierInfo(androidx.compose.ui.Modifier modifier, androidx.compose.ui.layout.LayoutCoordinates coordinates, Object? extra);
- method public androidx.compose.ui.layout.LayoutCoordinates getCoordinates();
- method public Object? getExtra();
- method public androidx.compose.ui.Modifier getModifier();
- property public final androidx.compose.ui.layout.LayoutCoordinates coordinates;
- property public final Object? extra;
- property public final androidx.compose.ui.Modifier modifier;
- }
-
public interface OwnedLayer {
method public void destroy();
method public void drawLayer(androidx.compose.ui.graphics.Canvas canvas);
@@ -2079,7 +2122,6 @@
method public androidx.compose.ui.focus.FocusManager getFocusManager();
method public androidx.compose.ui.text.font.Font.ResourceLoader getFontLoader();
method public androidx.compose.ui.hapticfeedback.HapticFeedback getHapticFeedBack();
- method public boolean getHasPendingMeasureOrLayout();
method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
method public long getMeasureIteration();
method public androidx.compose.ui.node.LayoutNode getRoot();
@@ -2097,7 +2139,7 @@
method public void onRequestRelayout(androidx.compose.ui.node.LayoutNode layoutNode);
method public void onSemanticsChange();
method public boolean requestFocus();
- method @androidx.compose.ui.input.key.ExperimentalKeyInput public boolean sendKeyEvent(androidx.compose.ui.input.key.KeyEvent keyEvent);
+ method public boolean sendKeyEvent(androidx.compose.ui.input.key.KeyEvent keyEvent);
property public abstract androidx.compose.ui.autofill.Autofill? autofill;
property public abstract androidx.compose.ui.autofill.AutofillTree autofillTree;
property public abstract androidx.compose.ui.platform.ClipboardManager clipboardManager;
@@ -2105,7 +2147,6 @@
property public abstract androidx.compose.ui.focus.FocusManager focusManager;
property public abstract androidx.compose.ui.text.font.Font.ResourceLoader fontLoader;
property public abstract androidx.compose.ui.hapticfeedback.HapticFeedback hapticFeedBack;
- property public abstract boolean hasPendingMeasureOrLayout;
property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
property public abstract long measureIteration;
property public abstract androidx.compose.ui.node.LayoutNode root;
@@ -2141,16 +2182,13 @@
property public final T? value;
}
- public final class UiApplier implements androidx.compose.runtime.Applier<java.lang.Object> {
+ public final class UiApplier extends androidx.compose.runtime.AbstractApplier<java.lang.Object> {
ctor public UiApplier(Object root);
- method public void clear();
- method public void down(Object node);
- method public Object getCurrent();
- method public void insert(int index, Object instance);
+ method public void insertBottomUp(int index, Object instance);
+ method public void insertTopDown(int index, Object instance);
method public void move(int from, int to, int count);
+ method protected void onClear();
method public void remove(int index, int count);
- method public void up();
- property public Object current;
}
public final class ViewInteropKt {
@@ -2188,8 +2226,6 @@
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ViewConfiguration> getAmbientViewConfiguration();
method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowManager> getAmbientWindowManager();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.animation.core.AnimationClockObservable>! getAnimationClockAmbient();
- method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.Autofill>! getAutofillAmbient();
- method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.autofill.AutofillTree>! getAutofillTreeAmbient();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ClipboardManager>! getClipboardManagerAmbient();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.unit.Density>! getDensityAmbient();
method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.focus.FocusManager>! getFocusManagerAmbient();
@@ -2221,31 +2257,6 @@
public final class AndroidComposeViewKt {
}
- public interface AndroidOwner extends androidx.compose.ui.node.Owner {
- method public void addAndroidView(androidx.compose.ui.viewinterop.AndroidViewHolder view, androidx.compose.ui.node.LayoutNode layoutNode);
- method public void drawAndroidView(androidx.compose.ui.viewinterop.AndroidViewHolder view, android.graphics.Canvas canvas);
- method public kotlin.jvm.functions.Function1<android.content.res.Configuration,kotlin.Unit> getConfigurationChangeObserver();
- method public android.view.View getView();
- method public androidx.compose.ui.platform.AndroidOwner.ViewTreeOwners? getViewTreeOwners();
- method public void invalidateDescendants();
- method public void removeAndroidView(androidx.compose.ui.viewinterop.AndroidViewHolder view);
- method public void setConfigurationChangeObserver(kotlin.jvm.functions.Function1<? super android.content.res.Configuration,kotlin.Unit> p);
- method public void setOnViewTreeOwnersAvailable(kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.AndroidOwner.ViewTreeOwners,kotlin.Unit> callback);
- property public abstract kotlin.jvm.functions.Function1<android.content.res.Configuration,kotlin.Unit> configurationChangeObserver;
- property public abstract android.view.View view;
- property public abstract androidx.compose.ui.platform.AndroidOwner.ViewTreeOwners? viewTreeOwners;
- }
-
- public static final class AndroidOwner.ViewTreeOwners {
- ctor public AndroidOwner.ViewTreeOwners(androidx.lifecycle.LifecycleOwner lifecycleOwner, androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner, androidx.savedstate.SavedStateRegistryOwner savedStateRegistryOwner);
- method public androidx.lifecycle.LifecycleOwner getLifecycleOwner();
- method public androidx.savedstate.SavedStateRegistryOwner getSavedStateRegistryOwner();
- method public androidx.lifecycle.ViewModelStoreOwner getViewModelStoreOwner();
- property public final androidx.lifecycle.LifecycleOwner lifecycleOwner;
- property public final androidx.savedstate.SavedStateRegistryOwner savedStateRegistryOwner;
- property public final androidx.lifecycle.ViewModelStoreOwner viewModelStoreOwner;
- }
-
public final class AndroidUriHandler implements androidx.compose.ui.platform.UriHandler {
ctor public AndroidUriHandler(android.content.Context context);
method public void openUri(String uri);
@@ -2324,10 +2335,6 @@
public final class JvmActualsKt {
}
- public final class SubcompositionKt {
- method @MainThread public static androidx.compose.runtime.Composition subcomposeInto(androidx.compose.ui.node.LayoutNode container, androidx.compose.runtime.CompositionReference parent, kotlin.jvm.functions.Function0<kotlin.Unit> composable);
- }
-
public final class TestTagKt {
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier testTag(androidx.compose.ui.Modifier, String tag);
}
@@ -2379,6 +2386,23 @@
property public abstract float touchSlop;
}
+ @VisibleForTesting public interface ViewRootForTest extends androidx.compose.ui.node.Owner {
+ method public boolean getHasPendingMeasureOrLayout();
+ method public android.view.View getView();
+ method public void invalidateDescendants();
+ method public boolean isLifecycleInResumedState();
+ property public abstract boolean hasPendingMeasureOrLayout;
+ property public abstract boolean isLifecycleInResumedState;
+ property public abstract android.view.View view;
+ field public static final androidx.compose.ui.platform.ViewRootForTest.Companion Companion;
+ }
+
+ public static final class ViewRootForTest.Companion {
+ method public kotlin.jvm.functions.Function1<androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? getOnViewCreatedCallback();
+ method public void setOnViewCreatedCallback(kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? p);
+ property public final kotlin.jvm.functions.Function1<androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? onViewCreatedCallback;
+ }
+
@androidx.compose.runtime.Stable public interface WindowManager {
method public boolean isWindowFocused();
property public abstract boolean isWindowFocused;
@@ -2474,7 +2498,7 @@
package androidx.compose.ui.selection {
- public interface Selectable {
+ @androidx.compose.ui.text.ExperimentalTextApi public interface Selectable {
method public androidx.compose.ui.geometry.Rect getBoundingBox(int offset);
method public long getHandlePosition-F1C5BW0(androidx.compose.ui.selection.Selection selection, boolean isStartHandle);
method public androidx.compose.ui.layout.LayoutCoordinates? getLayoutCoordinates();
@@ -2524,9 +2548,11 @@
public final class SelectionManagerKt {
}
- public interface SelectionRegistrar {
- method public void onPositionChange();
- method public void onUpdateSelection-rULFVbc(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition, long endPosition);
+ @androidx.compose.ui.text.ExperimentalTextApi public interface SelectionRegistrar {
+ method public void notifyPositionChange();
+ method public void notifySelectionUpdate-rULFVbc(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition, long endPosition);
+ method public void notifySelectionUpdateEnd();
+ method public void notifySelectionUpdateStart-YJiYy8w(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition);
method public androidx.compose.ui.selection.Selectable subscribe(androidx.compose.ui.selection.Selectable selectable);
method public void unsubscribe(androidx.compose.ui.selection.Selectable selectable);
}
@@ -2674,8 +2700,9 @@
method public androidx.compose.ui.geometry.Rect getGlobalBounds();
method public long getGlobalPosition-F1C5BW0();
method public int getId();
- method public androidx.compose.ui.node.LayoutNode getLayoutNode();
+ method public androidx.compose.ui.layout.LayoutInfo getLayoutInfo();
method public boolean getMergingEnabled();
+ method public androidx.compose.ui.node.Owner? getOwner();
method public androidx.compose.ui.semantics.SemanticsNode? getParent();
method public long getPositionInRoot-F1C5BW0();
method public long getSize-YbymL2g();
@@ -2687,8 +2714,9 @@
property public final long globalPosition;
property public final int id;
property public final boolean isRoot;
- property public final androidx.compose.ui.node.LayoutNode layoutNode;
+ property public final androidx.compose.ui.layout.LayoutInfo layoutInfo;
property public final boolean mergingEnabled;
+ property public final androidx.compose.ui.node.Owner? owner;
property public final androidx.compose.ui.semantics.SemanticsNode? parent;
property public final long positionInRoot;
property public final long size;
@@ -2711,9 +2739,8 @@
}
public final class SemanticsProperties {
- method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getAccessibilityLabel();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityRangeInfo> getAccessibilityRangeInfo();
- method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getAccessibilityValue();
+ method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getContentDescription();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getDisabled();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> getFocused();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getHidden();
@@ -2722,14 +2749,14 @@
method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getIsDialog();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> getIsPopup();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> getSelected();
+ method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getStateDescription();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> getTestTag();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.AnnotatedString> getText();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.TextRange> getTextSelectionRange();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.state.ToggleableState> getToggleableState();
method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityScrollState> getVerticalAccessibilityScrollState();
- property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> AccessibilityLabel;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityRangeInfo> AccessibilityRangeInfo;
- property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> AccessibilityValue;
+ property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> ContentDescription;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> Disabled;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> Focused;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> Hidden;
@@ -2738,6 +2765,7 @@
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> IsDialog;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<kotlin.Unit> IsPopup;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.Boolean> Selected;
+ property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> StateDescription;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.lang.String> TestTag;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.AnnotatedString> Text;
property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.text.TextRange> TextSelectionRange;
@@ -2752,14 +2780,16 @@
method public static void dialog(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void disabled(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void dismiss(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
- method public static String getAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
- method public static String getAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
- method public static androidx.compose.ui.semantics.AccessibilityRangeInfo getAccessibilityValueRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method @Deprecated public static String getAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method @Deprecated public static String getAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method public static String getContentDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction> getCustomActions(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static boolean getFocused(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static androidx.compose.ui.semantics.AccessibilityScrollState getHorizontalAccessibilityScrollState(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static androidx.compose.ui.text.input.ImeAction getImeAction(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static boolean getSelected(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method public static String getStateDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+ method public static androidx.compose.ui.semantics.AccessibilityRangeInfo getStateDescriptionRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static String getTestTag(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static androidx.compose.ui.text.AnnotatedString getText(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void getTextLayoutResult(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.text.TextLayoutResult>,java.lang.Boolean> action);
@@ -2772,9 +2802,9 @@
method public static void pasteText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
method public static void popup(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
method public static void scrollBy(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function2<? super java.lang.Float,? super java.lang.Float,java.lang.Boolean> action);
- method public static void setAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
- method public static void setAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
- method public static void setAccessibilityValueRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.semantics.AccessibilityRangeInfo p);
+ method @Deprecated public static void setAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
+ method @Deprecated public static void setAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
+ method public static void setContentDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
method public static void setCustomActions(androidx.compose.ui.semantics.SemanticsPropertyReceiver, java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction> p);
method public static void setFocused(androidx.compose.ui.semantics.SemanticsPropertyReceiver, boolean p);
method public static void setHorizontalAccessibilityScrollState(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.semantics.AccessibilityScrollState p);
@@ -2782,6 +2812,8 @@
method public static void setProgress(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Boolean> action);
method public static void setSelected(androidx.compose.ui.semantics.SemanticsPropertyReceiver, boolean p);
method public static void setSelection(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function3<? super java.lang.Integer,? super java.lang.Integer,? super java.lang.Boolean,java.lang.Boolean> action);
+ method public static void setStateDescription(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
+ method public static void setStateDescriptionRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.semantics.AccessibilityRangeInfo p);
method public static void setTestTag(androidx.compose.ui.semantics.SemanticsPropertyReceiver, String p);
method public static void setText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, androidx.compose.ui.text.AnnotatedString p);
method public static void setText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString,java.lang.Boolean> action);
diff --git a/compose/ui/ui/integration-tests/ui-demos/build.gradle b/compose/ui/ui/integration-tests/ui-demos/build.gradle
index eb67fb8..9a5536b 100644
--- a/compose/ui/ui/integration-tests/ui-demos/build.gradle
+++ b/compose/ui/ui/integration-tests/ui-demos/build.gradle
@@ -18,6 +18,7 @@
implementation project(":compose:foundation:foundation")
implementation project(":compose:foundation:foundation-layout")
implementation project(":compose:integration-tests:demos:common")
+ implementation project(":compose:ui:ui:ui-samples")
implementation project(":compose:material:material")
implementation project(":compose:runtime:runtime")
implementation project(":compose:runtime:runtime-livedata")
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
index 021cf64..5f74c0c 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
@@ -45,6 +45,7 @@
import androidx.compose.integration.demos.common.DemoCategory
import androidx.compose.ui.demos.focus.FocusInDialog
import androidx.compose.ui.demos.focus.FocusInPopup
+import androidx.compose.ui.samples.NestedScrollSample
private val GestureDemos = DemoCategory(
"Gestures",
@@ -87,7 +88,8 @@
ComposableDemo("Nested Long Press") { NestedLongPressDemo() },
ComposableDemo("Pointer Input During Sub Comp") { PointerInputDuringSubComp() }
)
- )
+ ),
+ ComposableDemo("New nested scroll") { NestedScrollSample() }
)
)
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt
index 9a32990..930bf45 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt
@@ -29,10 +29,10 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.AutofillNode
import androidx.compose.ui.autofill.AutofillType
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.isFocused
import androidx.compose.ui.focusObserver
import androidx.compose.ui.geometry.Offset
@@ -46,10 +46,7 @@
import androidx.compose.ui.unit.dp
@Composable
-@OptIn(
- ExperimentalFocus::class,
- ExperimentalFoundationApi::class
-)
+@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
fun ExplicitAutofillTypesDemo() {
Column {
val nameState = remember { mutableStateOf("Enter name here") }
@@ -112,6 +109,7 @@
}
}
+@ExperimentalComposeUiApi
@Composable
private fun Autofill(
autofillTypes: List<AutofillType>,
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusableDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusableDemo.kt
index efbbc4e..fd42463 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusableDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusableDemo.kt
@@ -28,7 +28,6 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.isFocused
import androidx.compose.ui.focusObserver
@@ -58,7 +57,6 @@
}
@Composable
-@OptIn(ExperimentalFocus::class)
private fun FocusableText(text: String) {
var color by remember { mutableStateOf(Black) }
val focusRequester = FocusRequester()
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/ReuseFocusRequester.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/ReuseFocusRequester.kt
index f0998a0..f3face6 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/ReuseFocusRequester.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/ReuseFocusRequester.kt
@@ -30,7 +30,6 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.isFocused
import androidx.compose.ui.focusObserver
@@ -44,7 +43,6 @@
private enum class CurrentShape { Circle, Square }
@Composable
-@OptIn(ExperimentalFocus::class)
fun ReuseFocusRequester() {
Column(
verticalArrangement = Arrangement.Top
@@ -76,7 +74,6 @@
}
@Composable
-@OptIn(ExperimentalFocus::class)
private fun Circle(modifier: Modifier = Modifier, nextShape: () -> Unit) {
var isFocused by remember { mutableStateOf(false) }
val scale = animate(if (isFocused) 0f else 1f, TweenSpec(2000)) {
@@ -100,7 +97,6 @@
}
@Composable
-@OptIn(ExperimentalFocus::class)
private fun Square(modifier: Modifier = Modifier, nextShape: () -> Unit) {
var isFocused by remember { mutableStateOf(false) }
val scale = animate(if (isFocused) 0f else 1f, TweenSpec(2000)) {
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/PointerInputDuringSubCompDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/PointerInputDuringSubCompDemo.kt
index f6ea061..8901b03 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/PointerInputDuringSubCompDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/PointerInputDuringSubCompDemo.kt
@@ -23,7 +23,7 @@
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.foundation.lazy.LazyColumnFor
+import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
@@ -60,27 +60,28 @@
"it is actually a new item that has not been hit tested yet. If you keep " +
"your finger there and then add more fingers, it will track those new fingers."
)
- LazyColumnFor(
- List(100) { index -> index },
+ LazyColumn(
Modifier
.fillMaxSize()
.wrapContentSize(Alignment.Center)
.size(200.dp)
.background(color = Color.White)
) {
- val pointerCount = remember { mutableStateOf(0) }
+ items(List(100) { index -> index }) {
+ val pointerCount = remember { mutableStateOf(0) }
- Box(
- Modifier.fillParentMaxSize()
- .border(width = 1.dp, color = Color.Black)
- .pointerCounterGestureFilter { newCount -> pointerCount.value = newCount },
- contentAlignment = Alignment.Center
- ) {
- Text(
- "${pointerCount.value}",
- fontSize = TextUnit.Em(16),
- color = Color.Black
- )
+ Box(
+ Modifier.fillParentMaxSize()
+ .border(width = 1.dp, color = Color.Black)
+ .pointerCounterGestureFilter { newCount -> pointerCount.value = newCount },
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ "${pointerCount.value}",
+ fontSize = TextUnit.Em(16),
+ color = Color.Black
+ )
+ }
}
}
}
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/keyinput/KeyInputDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/keyinput/KeyInputDemo.kt
index 4fe780d..9555fea 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/keyinput/KeyInputDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/keyinput/KeyInputDemo.kt
@@ -29,14 +29,12 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.isFocused
import androidx.compose.ui.focusObserver
import androidx.compose.ui.focusRequester
import androidx.compose.ui.gesture.tapGestureFilter
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.key.ExperimentalKeyInput
import androidx.compose.ui.input.key.KeyEventType.KeyDown
import androidx.compose.ui.input.key.keyInputFilter
@@ -63,10 +61,6 @@
}
@Composable
-@OptIn(
- ExperimentalFocus::class,
- ExperimentalKeyInput::class
-)
private fun FocusableText(text: MutableState<String>) {
var color by remember { mutableStateOf(Color.Black) }
val focusRequester = FocusRequester()
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/NestedScrollSamples.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/NestedScrollSamples.kt
new file mode 100644
index 0000000..4a95ba4
--- /dev/null
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/NestedScrollSamples.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+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.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.gesture.ScrollCallback
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollSource
+import androidx.compose.ui.gesture.nestedscroll.nestedScroll
+import androidx.compose.ui.gesture.scrollGestureFilter
+import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.minus
+import kotlin.math.roundToInt
+
+@Sampled
+@Composable
+fun NestedScrollSample() {
+ // constructing the box with next that scrolls as long as text within 0 .. 300
+ // to support nested scrolling, we need to scroll ourselves, dispatch nested scroll events
+ // as we scroll, and listen to potential children when we're scrolling.
+ val maxValue = 300f
+ val minValue = 0f
+ // our state that we update as scroll
+ var value by remember { mutableStateOf(maxValue / 2) }
+ // create dispatch to dispatch scroll events up to the nested scroll parents
+ val nestedScrollDispatcher = remember { NestedScrollDispatcher() }
+ // we're going to scroll vertically, so set the orientation to vertical
+ val orientation = Orientation.Vertical
+
+ // callback to listen to scroll events and dispatch nested scroll events
+ val scrollCallback = remember {
+ object : ScrollCallback {
+ override fun onScroll(scrollDistance: Float): Float {
+ // dispatch prescroll with Y axis since we're going vertical scroll
+ val aboveConsumed = nestedScrollDispatcher.dispatchPreScroll(
+ Offset(x = 0f, y = scrollDistance),
+ NestedScrollSource.Drag
+ )
+ // adjust what we can consume according to pre-scroll
+ val available = scrollDistance - aboveConsumed.y
+ // let's calculate how much we want to consume and how much is left
+ val newTotal = value + available
+ val newValue = newTotal.coerceIn(minValue, maxValue)
+ val toConsume = newValue - value
+ val leftAfterUs = available - toConsume
+ // consume ourselves what we need and dispatch "scroll" phase of nested scroll
+ value += toConsume
+ nestedScrollDispatcher.dispatchPostScroll(
+ Offset(x = 0f, y = toConsume),
+ Offset(x = 0f, y = leftAfterUs),
+ NestedScrollSource.Drag
+ )
+ // indicate to the old pointer that we handled everything by returning same value
+ return scrollDistance
+ }
+
+ override fun onStop(velocity: Float) {
+ // for simplicity we won't fling ourselves, but we need to respect nested scroll
+ // dispatch pre fling
+ val velocity2d = Velocity(Offset(x = 0f, y = velocity))
+ val consumed = nestedScrollDispatcher.dispatchPreFling(velocity2d)
+ // now, since we don't fling, we consume 0 (Offset.Zero).
+ // Adjust what's left after prefling and dispatch post fling
+ val left = velocity2d - consumed
+ nestedScrollDispatcher.dispatchPostFling(Velocity.Zero, left)
+ }
+ }
+ }
+
+ // we also want to participate in the nested scrolling, not only dispatching. create connection
+ val connection = remember {
+ object : NestedScrollConnection {
+ // let's assume we want to consume children's delta before them if we can
+ // we should do it in pre scroll
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ // calculate how much we can take from child
+ val oldValue = value
+ val newTotal = value + available.y
+ val newValue = newTotal.coerceIn(minValue, maxValue)
+ val toConsume = newValue - oldValue
+ // consume what we want and report back co children can adjust
+ value += toConsume
+ return Offset(x = 0f, y = toConsume)
+ }
+ }
+ }
+
+ // scrollable parent to which we will dispatch our nested scroll events
+ // Since we properly support scrolling above, this parent will scroll even if we scroll inner
+ // box (with White background)
+ LazyColumn(Modifier.background(Color.Red)) {
+ // our box we constructed
+ item {
+ Box(
+ Modifier
+ .size(width = 300.dp, height = 100.dp)
+ .background(Color.White)
+ // add scrolling listening and dispatching
+ .scrollGestureFilter(orientation = orientation, scrollCallback = scrollCallback)
+ // connect self connection and dispatcher to the nested scrolling system
+ .nestedScroll(connection, dispatcher = nestedScrollDispatcher)
+ ) {
+ // hypothetical scrollable child which we will listen in connection above
+ LazyColumn {
+ items(listOf(1, 2, 3, 4, 5)) {
+ Text(
+ "Magenta text above will change first when you scroll me",
+ modifier = Modifier.padding(5.dp)
+ )
+ }
+ }
+ // simply show our value. It will change when we scroll child list above, taking
+ // child's scroll delta until we reach maxValue or minValue
+ Text(
+ text = value.roundToInt().toString(),
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(Color.Magenta)
+ )
+ }
+ }
+ repeat(100) {
+ item {
+ Text(
+ "Outer scroll items are Yellow on Red parent",
+ modifier = Modifier.background(Color.Yellow).padding(5.dp)
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
index 40ac0f4..8fb5451 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
@@ -58,7 +58,6 @@
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
-import androidx.core.os.BuildCompat
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -174,7 +173,7 @@
var accessibilityNodeInfo = provider.createAccessibilityNodeInfo(toggleableNode.id)
assertEquals("android.view.View", accessibilityNodeInfo.className)
val stateDescription = when {
- BuildCompat.isAtLeastR() -> {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
accessibilityNodeInfo.stateDescription
}
Build.VERSION.SDK_INT >= 19 -> {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
index 480bae1..0802cb8 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
@@ -34,8 +34,8 @@
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.SemanticsWrapper
-import androidx.compose.ui.semantics.accessibilityValue
-import androidx.compose.ui.semantics.accessibilityValueRange
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.semantics.stateDescriptionRange
import androidx.compose.ui.semantics.dismiss
import androidx.compose.ui.semantics.focused
import androidx.compose.ui.semantics.getTextLayoutResult
@@ -50,7 +50,6 @@
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextRange
-import androidx.core.os.BuildCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -111,9 +110,9 @@
val info = AccessibilityNodeInfoCompat.obtain()
val clickActionLabel = "click"
val dismissActionLabel = "dismiss"
- val accessibilityValue = "checked"
+ val stateDescription = "checked"
val semanticsModifier = SemanticsModifierCore(1, true, false) {
- this.accessibilityValue = accessibilityValue
+ this.stateDescription = stateDescription
onClick(clickActionLabel) { true }
dismiss(dismissActionLabel) { true }
}
@@ -141,8 +140,8 @@
)
)
)
- val stateDescription = when {
- BuildCompat.isAtLeastR() -> {
+ val stateDescriptionResult = when {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
info.unwrap().stateDescription
}
Build.VERSION.SDK_INT >= 19 -> {
@@ -154,7 +153,7 @@
null
}
}
- assertEquals(accessibilityValue, stateDescription)
+ assertEquals(stateDescription, stateDescriptionResult)
assertTrue(info.isClickable)
assertTrue(info.isVisibleToUser)
}
@@ -164,7 +163,7 @@
val info = AccessibilityNodeInfoCompat.obtain()
val setProgressActionLabel = "setProgress"
val semanticsModifier = SemanticsModifierCore(1, true, false) {
- accessibilityValueRange = AccessibilityRangeInfo(0.5f, 0f..1f, 6)
+ stateDescriptionRange = AccessibilityRangeInfo(0.5f, 0f..1f, 6)
setProgress(setProgressActionLabel) { true }
}
val semanticsNode = SemanticsNode(
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
index f5aa880..8119ae9 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/autofill/AndroidAutoFillTest.kt
@@ -22,6 +22,7 @@
import android.view.autofill.AutofillValue
import androidx.autofill.HintConstants.AUTOFILL_HINT_PERSON_NAME
import androidx.compose.testutils.fake.FakeViewStructure
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.platform.AmbientAutofill
import androidx.compose.ui.platform.AmbientAutofillTree
@@ -36,6 +37,7 @@
import org.junit.Test
import org.junit.runner.RunWith
+@OptIn(ExperimentalComposeUiApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class AndroidAutoFillTest {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CaptureFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CaptureFocusTest.kt
index 4dfe5bb..f85758d 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CaptureFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CaptureFocusTest.kt
@@ -30,7 +30,6 @@
import org.junit.runner.RunWith
@MediumTest
-@OptIn(ExperimentalFocus::class)
@RunWith(AndroidJUnit4::class)
class CaptureFocusTest {
@get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ClearFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ClearFocusTest.kt
index 382c8fa..238100b 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ClearFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ClearFocusTest.kt
@@ -32,7 +32,6 @@
import org.junit.runners.Parameterized
@SmallTest
-@OptIn(ExperimentalFocus::class)
@RunWith(Parameterized::class)
class ClearFocusTest(val forcedClear: Boolean) {
@get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindFocusableChildrenTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindFocusableChildrenTest.kt
index ff6adc0..121fc03 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindFocusableChildrenTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindFocusableChildrenTest.kt
@@ -30,7 +30,6 @@
import org.junit.runner.RunWith
@MediumTest
-@OptIn(ExperimentalFocus::class)
@RunWith(AndroidJUnit4::class)
class FindFocusableChildrenTest {
@get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindParentFocusNodeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindParentFocusNodeTest.kt
index 24f8ee1..7db84d4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindParentFocusNodeTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindParentFocusNodeTest.kt
@@ -30,7 +30,6 @@
import org.junit.runner.RunWith
@MediumTest
-@OptIn(ExperimentalFocus::class)
@RunWith(AndroidJUnit4::class)
class FindParentFocusNodeTest {
@get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusManagerAmbientTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusManagerAmbientTest.kt
index ccf7260..5a2f75b 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusManagerAmbientTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusManagerAmbientTest.kt
@@ -34,7 +34,6 @@
import org.junit.runner.RunWith
@MediumTest
-@OptIn(ExperimentalFocus::class)
@RunWith(AndroidJUnit4::class)
class FocusManagerAmbientTest {
@get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusModifierAttachDetachTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusModifierAttachDetachTest.kt
index b8f96cb..c9cf7a2 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusModifierAttachDetachTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusModifierAttachDetachTest.kt
@@ -37,7 +37,6 @@
import org.junit.runner.RunWith
@MediumTest
-@OptIn(ExperimentalFocus::class)
@RunWith(AndroidJUnit4::class)
class FocusModifierAttachDetachTest {
@get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusObserverTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusObserverTest.kt
index dee66f9..ba18ff1 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusObserverTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusObserverTest.kt
@@ -36,7 +36,6 @@
import org.junit.runner.RunWith
@MediumTest
-@OptIn(ExperimentalFocus::class)
@RunWith(AndroidJUnit4::class)
class FocusObserverTest {
@get:Rule
@@ -69,8 +68,7 @@
fun activeParent_requestFocus() {
// Arrange.
lateinit var focusState: FocusState
- val focusRequester = FocusRequester()
- val childFocusRequester = FocusRequester()
+ val (focusRequester, childFocusRequester) = FocusRequester.createRefs()
rule.setFocusableContent {
Box(
modifier = Modifier
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRequesterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRequesterTest.kt
index 3f04cfa..a0cc2da 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRequesterTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRequesterTest.kt
@@ -35,7 +35,6 @@
import org.junit.runner.RunWith
@MediumTest
-@OptIn(ExperimentalFocus::class)
@RunWith(AndroidJUnit4::class)
class FocusRequesterTest {
@get:Rule
@@ -259,8 +258,7 @@
// Arrange.
lateinit var hostView: View
var focusState = Inactive
- val focusRequester1 = FocusRequester()
- val focusRequester2 = FocusRequester()
+ val (focusRequester1, focusRequester2) = FocusRequester.createRefs()
rule.setFocusableContent {
hostView = AmbientView.current
Column(
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FreeFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FreeFocusTest.kt
index bd92c83..15b2061 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FreeFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FreeFocusTest.kt
@@ -35,7 +35,6 @@
import org.junit.runner.RunWith
@MediumTest
-@OptIn(ExperimentalFocus::class)
@RunWith(AndroidJUnit4::class)
class FreeFocusTest {
@get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
index 88a6397..ad52c75 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
@@ -36,7 +36,6 @@
import org.junit.runner.RunWith
@MediumTest
-@OptIn(ExperimentalFocus::class)
@RunWith(AndroidJUnit4::class)
class OwnerFocusTest {
@get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusTest.kt
index 8f92466..a7c6af1 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusTest.kt
@@ -32,7 +32,6 @@
import org.junit.runners.Parameterized
@SmallTest
-@OptIn(ExperimentalFocus::class)
@RunWith(Parameterized::class)
class RequestFocusTest(val propagateFocus: Boolean) {
@get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterCaptureFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterCaptureFocusTest.kt
index 6c3630e..fcc4439 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterCaptureFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterCaptureFocusTest.kt
@@ -33,7 +33,6 @@
import org.junit.runner.RunWith
@MediumTest
-@OptIn(ExperimentalFocus::class)
@RunWith(AndroidJUnit4::class)
class ReusedFocusRequesterCaptureFocusTest {
@get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterFreeFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterFreeFocusTest.kt
index 8c652a1..9251e3f 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterFreeFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterFreeFocusTest.kt
@@ -33,7 +33,6 @@
import org.junit.runner.RunWith
@MediumTest
-@OptIn(ExperimentalFocus::class)
@RunWith(AndroidJUnit4::class)
class ReusedFocusRequesterFreeFocusTest {
@get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterTest.kt
index 0993a57..50349f4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterTest.kt
@@ -32,7 +32,6 @@
import org.junit.runner.RunWith
@MediumTest
-@OptIn(ExperimentalFocus::class)
@RunWith(AndroidJUnit4::class)
class ReusedFocusRequesterTest {
@get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/SetRootFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/SetRootFocusTest.kt
index d6a6633..43f5cc0 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/SetRootFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/SetRootFocusTest.kt
@@ -36,7 +36,6 @@
import org.junit.runner.RunWith
@MediumTest
-@OptIn(ExperimentalFocus::class)
@RunWith(AndroidJUnit4::class)
class SetRootFocusTest {
@get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/gesture/nestedscroll/NestedScrollModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/gesture/nestedscroll/NestedScrollModifierTest.kt
new file mode 100644
index 0000000..513b0f6
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/gesture/nestedscroll/NestedScrollModifierTest.kt
@@ -0,0 +1,1062 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.gesture.nestedscroll
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.minus
+import androidx.compose.ui.unit.plus
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class NestedScrollModifierTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ private val preScrollOffset = Offset(120f, 120f)
+ private val scrollOffset = Offset(125f, 125f)
+ private val scrollLeftOffset = Offset(32f, 32f)
+ private val preFling = Velocity(Offset(120f, 120f))
+ private val postFlingConsumed = Velocity(Offset(151f, 63f))
+ private val postFlingLeft = Velocity(Offset(11f, 13f))
+
+ @Test
+ fun nestedScroll_twoNodes_orderTest() {
+ var counter = 0
+ val childConnection = object : NestedScrollConnection {}
+ val parentConnection = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ assertThat(counter).isEqualTo(1)
+ counter++
+ return Offset.Zero
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ assertThat(counter).isEqualTo(3)
+ counter++
+ return Offset.Zero
+ }
+
+ override fun onPreFling(available: Velocity): Velocity {
+ assertThat(counter).isEqualTo(5)
+ counter++
+ return Velocity.Zero
+ }
+
+ override fun onPostFling(
+ consumed: Velocity,
+ available: Velocity,
+ onFinished: (Velocity) -> Unit
+ ) {
+ assertThat(counter).isEqualTo(7)
+ counter++
+ }
+ }
+ val childDispatcher = NestedScrollDispatcher()
+ rule.setContent {
+ Box(Modifier.size(100.dp).nestedScroll(parentConnection)) {
+ Box(
+ Modifier.size(100.dp).nestedScroll(childConnection, childDispatcher)
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(counter).isEqualTo(0)
+ counter++
+
+ childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ assertThat(counter).isEqualTo(2)
+ counter++
+
+ childDispatcher
+ .dispatchPostScroll(scrollOffset, scrollLeftOffset, NestedScrollSource.Drag)
+ assertThat(counter).isEqualTo(4)
+ counter++
+
+ childDispatcher.dispatchPreFling(preFling)
+ assertThat(counter).isEqualTo(6)
+ counter++
+
+ childDispatcher.dispatchPostFling(postFlingConsumed, postFlingLeft)
+ assertThat(counter).isEqualTo(8)
+ counter++
+ }
+ }
+
+ @Test
+ fun nestedScroll_NNodes_orderTest_preScroll() {
+ var counter = 0
+ val childConnection = object : NestedScrollConnection {}
+ val parentConnection = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ assertThat(counter).isEqualTo(2)
+ counter++
+ return Offset.Zero
+ }
+ }
+ val grandParentConnection = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ assertThat(counter).isEqualTo(1)
+ counter++
+ return Offset.Zero
+ }
+ }
+ val childDispatcher = NestedScrollDispatcher()
+ rule.setContent {
+ Box(Modifier.size(100.dp).nestedScroll(grandParentConnection)) {
+ Box(Modifier.size(100.dp).nestedScroll(parentConnection)) {
+ Box(
+ Modifier.size(100.dp).nestedScroll(childConnection, childDispatcher)
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(counter).isEqualTo(0)
+ counter++
+
+ childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ assertThat(counter).isEqualTo(3)
+ counter++
+ }
+ }
+
+ @Test
+ fun nestedScroll_NNodes_orderTest_scroll() {
+ var counter = 0
+ val childConnection = object : NestedScrollConnection {}
+ val parentConnection = object : NestedScrollConnection {
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ assertThat(counter).isEqualTo(1)
+ counter++
+ return Offset.Zero
+ }
+ }
+ val grandParentConnection = object : NestedScrollConnection {
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ assertThat(counter).isEqualTo(2)
+ counter++
+ return Offset.Zero
+ }
+ }
+ val childDispatcher = NestedScrollDispatcher()
+ rule.setContent {
+ Box(Modifier.size(100.dp).nestedScroll(grandParentConnection)) {
+ Box(Modifier.size(100.dp).nestedScroll(parentConnection)) {
+ Box(
+ Modifier.size(100.dp).nestedScroll(childConnection, childDispatcher)
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(counter).isEqualTo(0)
+ counter++
+
+ childDispatcher
+ .dispatchPostScroll(scrollOffset, scrollLeftOffset, NestedScrollSource.Drag)
+ assertThat(counter).isEqualTo(3)
+ counter++
+ }
+ }
+
+ @Test
+ fun nestedScroll_NNodes_orderTest_preFling() {
+ var counter = 0
+ val childConnection = object : NestedScrollConnection {}
+ val parentConnection = object : NestedScrollConnection {
+
+ override fun onPreFling(available: Velocity): Velocity {
+ assertThat(counter).isEqualTo(2)
+ counter++
+ return Velocity.Zero
+ }
+ }
+ val grandParentConnection = object : NestedScrollConnection {
+ override fun onPreFling(available: Velocity): Velocity {
+ assertThat(counter).isEqualTo(1)
+ counter++
+ return Velocity.Zero
+ }
+ }
+ val childDispatcher = NestedScrollDispatcher()
+ rule.setContent {
+ Box(Modifier.size(100.dp).nestedScroll(grandParentConnection)) {
+ Box(Modifier.size(100.dp).nestedScroll(parentConnection)) {
+ Box(
+ Modifier.size(100.dp).nestedScroll(childConnection, childDispatcher)
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(counter).isEqualTo(0)
+ counter++
+
+ childDispatcher.dispatchPreFling(preFling)
+ assertThat(counter).isEqualTo(3)
+ counter++
+ }
+ }
+
+ @Test
+ fun nestedScroll_NNodes_orderTest_fling() {
+ var counter = 0
+ val childConnection = object : NestedScrollConnection {}
+ val parentConnection = object : NestedScrollConnection {
+ override fun onPostFling(
+ consumed: Velocity,
+ available: Velocity,
+ onFinished: (Velocity) -> Unit
+ ) {
+ assertThat(counter).isEqualTo(1)
+ counter++
+ onFinished.invoke(Velocity.Zero)
+ }
+ }
+ val grandParentConnection = object : NestedScrollConnection {
+ override fun onPostFling(
+ consumed: Velocity,
+ available: Velocity,
+ onFinished: (Velocity) -> Unit
+ ) {
+ assertThat(counter).isEqualTo(2)
+ counter++
+ onFinished.invoke(Velocity.Zero)
+ }
+ }
+ val childDispatcher = NestedScrollDispatcher()
+ rule.setContent {
+ Box(Modifier.size(100.dp).nestedScroll(grandParentConnection)) {
+ Box(Modifier.size(100.dp).nestedScroll(parentConnection)) {
+ Box(
+ Modifier.size(100.dp).nestedScroll(childConnection, childDispatcher)
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(counter).isEqualTo(0)
+ counter++
+
+ childDispatcher.dispatchPostFling(postFlingConsumed, postFlingLeft)
+
+ assertThat(counter).isEqualTo(3)
+ counter++
+ }
+ }
+
+ @Test
+ fun nestedScroll_twoNodes_hierarchyDispatch() {
+ val preScrollReturn = Offset(60f, 30f)
+ val preFlingReturn = Velocity(Offset(154f, 56f))
+ var currentsource = NestedScrollSource.Drag
+
+ val childConnection = object : NestedScrollConnection {}
+ val parentConnection = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ assertThat(available).isEqualTo(preScrollOffset)
+ assertThat(source).isEqualTo(currentsource)
+ return preScrollReturn
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ assertThat(consumed).isEqualTo(scrollOffset)
+ assertThat(available).isEqualTo(scrollLeftOffset)
+ assertThat(source).isEqualTo(currentsource)
+ return Offset.Zero
+ }
+
+ override fun onPreFling(available: Velocity): Velocity {
+ assertThat(available).isEqualTo(preFling)
+ return preFlingReturn
+ }
+
+ override fun onPostFling(
+ consumed: Velocity,
+ available: Velocity,
+ onFinished: (Velocity) -> Unit
+ ) {
+ assertThat(consumed).isEqualTo(postFlingConsumed)
+ assertThat(available).isEqualTo(postFlingLeft)
+ }
+ }
+ val childDispatcher = NestedScrollDispatcher()
+ rule.setContent {
+ Box(Modifier.size(100.dp).nestedScroll(parentConnection)) {
+ Box(
+ Modifier.size(100.dp).nestedScroll(childConnection, childDispatcher)
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ val preRes = childDispatcher.dispatchPreScroll(preScrollOffset, currentsource)
+ assertThat(preRes).isEqualTo(preScrollReturn)
+
+ childDispatcher.dispatchPostScroll(scrollOffset, scrollLeftOffset, currentsource)
+ // flip to fling to test again below
+ currentsource = NestedScrollSource.Fling
+ }
+
+ rule.runOnIdle {
+ val preRes = childDispatcher.dispatchPreScroll(preScrollOffset, currentsource)
+ assertThat(preRes).isEqualTo(preScrollReturn)
+
+ childDispatcher.dispatchPostScroll(scrollOffset, scrollLeftOffset, currentsource)
+ }
+
+ rule.runOnIdle {
+ val preFlingRes = childDispatcher.dispatchPreFling(preFling)
+ assertThat(preFlingRes).isEqualTo(preFlingReturn)
+ childDispatcher.dispatchPostFling(postFlingConsumed, postFlingLeft)
+ }
+ }
+
+ @Test
+ fun nestedScroll_deltaCalculation_preScroll() {
+ val dispatchedPreScroll = Offset(10f, 10f)
+ val grandParentConsumesPreScroll = Offset(2f, 2f)
+ val parentConsumedPreScroll = Offset(1f, 1f)
+
+ val childConnection = object : NestedScrollConnection {}
+ val grandParentConnection = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ assertThat(available).isEqualTo(dispatchedPreScroll)
+ return grandParentConsumesPreScroll
+ }
+ }
+ val parentConnection = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ assertThat(available)
+ .isEqualTo(dispatchedPreScroll - grandParentConsumesPreScroll)
+ return parentConsumedPreScroll
+ }
+ }
+ val childDispatcher = NestedScrollDispatcher()
+ rule.setContent {
+ Box(Modifier.size(100.dp).nestedScroll(grandParentConnection)) {
+ Box(Modifier.size(100.dp).nestedScroll(parentConnection)) {
+ Box(
+ Modifier.size(100.dp).nestedScroll(childConnection, childDispatcher)
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ val preRes =
+ childDispatcher.dispatchPreScroll(dispatchedPreScroll, NestedScrollSource.Drag)
+ assertThat(preRes).isEqualTo(grandParentConsumesPreScroll + parentConsumedPreScroll)
+ }
+ }
+
+ @Test
+ fun nestedScroll_deltaCalculation_scroll() {
+ val dispatchedConsumedScroll = Offset(4f, 4f)
+ val dispatchedScroll = Offset(10f, 10f)
+ val grandParentConsumedScroll = Offset(2f, 2f)
+ val parentConsumedScroll = Offset(1f, 1f)
+
+ val childConnection = object : NestedScrollConnection {}
+ val grandParentConnection = object : NestedScrollConnection {
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ assertThat(consumed).isEqualTo(parentConsumedScroll + dispatchedConsumedScroll)
+ assertThat(available).isEqualTo(dispatchedScroll - parentConsumedScroll)
+ return grandParentConsumedScroll
+ }
+ }
+ val parentConnection = object : NestedScrollConnection {
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ assertThat(consumed).isEqualTo(dispatchedConsumedScroll)
+ assertThat(available).isEqualTo(dispatchedScroll)
+ return parentConsumedScroll
+ }
+ }
+ val childDispatcher = NestedScrollDispatcher()
+ rule.setContent {
+ Box(Modifier.size(100.dp).nestedScroll(grandParentConnection)) {
+ Box(Modifier.size(100.dp).nestedScroll(parentConnection)) {
+ Box(
+ Modifier.size(100.dp).nestedScroll(childConnection, childDispatcher)
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ childDispatcher.dispatchPostScroll(
+ dispatchedConsumedScroll,
+ dispatchedScroll,
+ NestedScrollSource.Drag
+ )
+ }
+ }
+
+ @Test
+ fun nestedScroll_deltaCalculation_preFling() {
+ val dispatchedVelocity = Velocity(Offset(10f, 10f))
+ val grandParentConsumesPreFling = Velocity(Offset(2f, 2f))
+ val parentConsumedPreFling = Velocity(Offset(1f, 1f))
+
+ val childConnection = object : NestedScrollConnection {}
+ val grandParentConnection = object : NestedScrollConnection {
+ override fun onPreFling(available: Velocity): Velocity {
+ assertThat(available).isEqualTo(dispatchedVelocity)
+ return grandParentConsumesPreFling
+ }
+ }
+ val parentConnection = object : NestedScrollConnection {
+ override fun onPreFling(available: Velocity): Velocity {
+ assertThat(available)
+ .isEqualTo(dispatchedVelocity - grandParentConsumesPreFling)
+ return parentConsumedPreFling
+ }
+ }
+ val childDispatcher = NestedScrollDispatcher()
+ rule.setContent {
+ Box(Modifier.size(100.dp).nestedScroll(grandParentConnection)) {
+ Box(Modifier.size(100.dp).nestedScroll(parentConnection)) {
+ Box(
+ Modifier.size(100.dp).nestedScroll(childConnection, childDispatcher)
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ val preRes = childDispatcher.dispatchPreFling(dispatchedVelocity)
+ assertThat(preRes).isEqualTo(grandParentConsumesPreFling + parentConsumedPreFling)
+ }
+ }
+
+ @Test
+ fun nestedScroll_deltaCalculation_fling() {
+ val dispatchedConsumedVelocity = Velocity(Offset(4f, 4f))
+ val dispatchedLeftVelocity = Velocity(Offset(10f, 10f))
+ val grandParentConsumedPostFling = Velocity(Offset(2f, 2f))
+ val parentConsumedPostFling = Velocity(Offset(1f, 1f))
+
+ val childConnection = object : NestedScrollConnection {}
+ val grandParentConnection = object : NestedScrollConnection {
+ override fun onPostFling(
+ consumed: Velocity,
+ available: Velocity,
+ onFinished: (Velocity) -> Unit
+ ) {
+ assertThat(consumed)
+ .isEqualTo(parentConsumedPostFling + dispatchedConsumedVelocity)
+ assertThat(available)
+ .isEqualTo(dispatchedLeftVelocity - parentConsumedPostFling)
+ return onFinished(grandParentConsumedPostFling)
+ }
+ }
+ val parentConnection = object : NestedScrollConnection {
+
+ override fun onPostFling(
+ consumed: Velocity,
+ available: Velocity,
+ onFinished: (Velocity) -> Unit
+ ) {
+ assertThat(consumed).isEqualTo(dispatchedConsumedVelocity)
+ assertThat(available).isEqualTo(dispatchedLeftVelocity)
+ onFinished(parentConsumedPostFling)
+ }
+ }
+ val childDispatcher = NestedScrollDispatcher()
+ rule.setContent {
+ Box(Modifier.size(100.dp).nestedScroll(grandParentConnection)) {
+ Box(Modifier.size(100.dp).nestedScroll(parentConnection)) {
+ Box(
+ Modifier.size(100.dp).nestedScroll(childConnection, childDispatcher)
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ childDispatcher.dispatchPostFling(dispatchedConsumedVelocity, dispatchedLeftVelocity)
+ }
+ }
+
+ @Test
+ fun nestedScroll_twoNodes_flatDispatch() {
+ val preScrollReturn = Offset(60f, 30f)
+ val preFlingReturn = Velocity(Offset(154f, 56f))
+ var currentsource = NestedScrollSource.Drag
+
+ val childConnection = object : NestedScrollConnection {}
+ val parentConnection = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ assertThat(available).isEqualTo(preScrollOffset)
+ assertThat(source).isEqualTo(currentsource)
+ return preScrollReturn
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ assertThat(consumed).isEqualTo(scrollOffset)
+ assertThat(available).isEqualTo(scrollLeftOffset)
+ assertThat(source).isEqualTo(currentsource)
+ return Offset.Zero
+ }
+
+ override fun onPreFling(available: Velocity): Velocity {
+ assertThat(available).isEqualTo(preFling)
+ return preFlingReturn
+ }
+
+ override fun onPostFling(
+ consumed: Velocity,
+ available: Velocity,
+ onFinished: (Velocity) -> Unit
+ ) {
+ assertThat(consumed).isEqualTo(postFlingConsumed)
+ assertThat(available).isEqualTo(postFlingLeft)
+ }
+ }
+ val childDispatcher = NestedScrollDispatcher()
+ rule.setContent {
+ Box(
+ Modifier
+ .size(100.dp)
+ .nestedScroll(parentConnection) // parent
+ .nestedScroll(childConnection, childDispatcher) // child
+ )
+ }
+
+ rule.runOnIdle {
+ val preRes = childDispatcher.dispatchPreScroll(preScrollOffset, currentsource)
+ assertThat(preRes).isEqualTo(preScrollReturn)
+
+ childDispatcher.dispatchPostScroll(scrollOffset, scrollLeftOffset, currentsource)
+ // flip to fling to test again below
+ currentsource = NestedScrollSource.Fling
+ }
+
+ rule.runOnIdle {
+ val preRes = childDispatcher.dispatchPreScroll(preScrollOffset, currentsource)
+ assertThat(preRes).isEqualTo(preScrollReturn)
+
+ childDispatcher.dispatchPostScroll(scrollOffset, scrollLeftOffset, currentsource)
+ }
+
+ rule.runOnIdle {
+ val preFlingRes = childDispatcher.dispatchPreFling(preFling)
+ assertThat(preFlingRes).isEqualTo(preFlingReturn)
+ childDispatcher.dispatchPostFling(postFlingConsumed, postFlingLeft)
+ }
+ }
+
+ @Test
+ fun nestedScroll_shouldNotCalledSelfConnection() {
+ val childConnection = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ assertWithMessage("self connection shouldn't be called").fail()
+ return Offset.Zero
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ assertWithMessage("self connection shouldn't be called").fail()
+ return Offset.Zero
+ }
+
+ override fun onPreFling(available: Velocity): Velocity {
+ assertWithMessage("self connection shouldn't be called").fail()
+ return Velocity.Zero
+ }
+
+ override fun onPostFling(
+ consumed: Velocity,
+ available: Velocity,
+ onFinished: (Velocity) -> Unit
+ ) {
+ assertWithMessage("self connection shouldn't be called").fail()
+ }
+ }
+ val parentConnection = object : NestedScrollConnection {}
+ val childDispatcher = NestedScrollDispatcher()
+ rule.setContent {
+ Box(Modifier.nestedScroll(parentConnection)) {
+ Box(Modifier.nestedScroll(childConnection, childDispatcher))
+ }
+ }
+
+ rule.runOnIdle {
+ childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ childDispatcher
+ .dispatchPostScroll(scrollOffset, scrollLeftOffset, NestedScrollSource.Fling)
+ }
+
+ rule.runOnIdle {
+ childDispatcher.dispatchPreFling(preFling)
+ childDispatcher.dispatchPostFling(postFlingConsumed, postFlingLeft)
+ }
+ }
+
+ @Test
+ fun nestedScroll_hierarchyDispatch_rootParentRemoval() {
+ testRootParentAdditionRemoval { root, child ->
+ Box(Modifier.size(100.dp).then(root)) {
+ Box(child)
+ }
+ }
+ }
+
+ @Test
+ fun nestedScroll_flatDispatch_rootParentRemoval() {
+ testRootParentAdditionRemoval { root, child ->
+ Box(Modifier.then(root).then(child))
+ }
+ }
+
+ @Test
+ fun nestedScroll_flatDispatch_longChain_rootParentRemoval() {
+ testRootParentAdditionRemoval { root, child ->
+ // insert a few random modifiers so it's more realistic example of wrapper re-usage
+ Box(Modifier.size(100.dp).then(root).padding(5.dp).size(50.dp).then(child))
+ }
+ }
+
+ @Test
+ fun nestedScroll_hierarchyDispatch_middleParentRemoval() {
+ testMiddleParentAdditionRemoval { rootMod, middleMod, childMod ->
+ // random boxes to emulate nesting
+ Box(Modifier.size(100.dp).then(rootMod)) {
+ Box {
+ Box(Modifier.size(100.dp).then(middleMod)) {
+ Box {
+ Box(childMod)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun nestedScroll_flatDispatch_middleParentRemoval() {
+ testMiddleParentAdditionRemoval { rootMod, middleMod, childMod ->
+ Box(
+ Modifier
+ .then(rootMod)
+ .then(middleMod)
+ .then(childMod)
+ )
+ }
+ }
+
+ @Test
+ fun nestedScroll_flatDispatch_longChain_middleParentRemoval() {
+ testMiddleParentAdditionRemoval { rootMod, middleMod, childMod ->
+ // insert a few random modifiers so it's more realistic example of wrapper re-usage
+ Box(
+ Modifier
+ .size(100.dp)
+ .then(rootMod)
+ .size(90.dp)
+ .clipToBounds()
+ .then(middleMod)
+ .padding(5.dp)
+ .then(childMod)
+ )
+ }
+ }
+
+ @Test
+ fun nestedScroll_flatDispatch_runtimeSwapChange_orderTest() {
+ val preScrollReturn = Offset(60f, 30f)
+ val preFlingReturn = Velocity(Offset(154f, 56f))
+ var counter = 0
+
+ val isConnection1Parent = mutableStateOf(true)
+ val childConnection = object : NestedScrollConnection {}
+ val connection1 = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ assertThat(counter).isEqualTo(if (isConnection1Parent.value) 1 else 2)
+ counter++
+ return preScrollReturn
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ assertThat(counter).isEqualTo(if (isConnection1Parent.value) 2 else 1)
+ counter++
+ return Offset.Zero
+ }
+
+ override fun onPreFling(available: Velocity): Velocity {
+ assertThat(counter).isEqualTo(if (isConnection1Parent.value) 1 else 2)
+ counter++
+ return preFlingReturn
+ }
+
+ override fun onPostFling(
+ consumed: Velocity,
+ available: Velocity,
+ onFinished: (Velocity) -> Unit
+ ) {
+ assertThat(counter).isEqualTo(if (isConnection1Parent.value) 2 else 1)
+ counter++
+ onFinished.invoke(Velocity.Zero)
+ }
+ }
+ val connection2 = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ assertThat(counter).isEqualTo(if (isConnection1Parent.value) 2 else 1)
+ counter++
+ return preScrollReturn
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ assertThat(counter).isEqualTo(if (isConnection1Parent.value) 1 else 2)
+ counter++
+ return Offset.Zero
+ }
+
+ override fun onPreFling(available: Velocity): Velocity {
+ assertThat(counter).isEqualTo(if (isConnection1Parent.value) 2 else 1)
+ counter++
+ return preFlingReturn
+ }
+
+ override fun onPostFling(
+ consumed: Velocity,
+ available: Velocity,
+ onFinished: (Velocity) -> Unit
+ ) {
+ assertThat(counter).isEqualTo(if (isConnection1Parent.value) 1 else 2)
+ counter++
+ onFinished.invoke(Velocity.Zero)
+ }
+ }
+ val childDispatcher = NestedScrollDispatcher()
+ rule.setContent {
+ val nestedScrollParents = if (isConnection1Parent.value) {
+ Modifier.nestedScroll(connection1).nestedScroll(connection2)
+ } else {
+ Modifier.nestedScroll(connection2).nestedScroll(connection1)
+ }
+ Box(
+ Modifier
+ .size(100.dp)
+ .then(nestedScrollParents)
+ .nestedScroll(childConnection, childDispatcher)
+ )
+ }
+
+ repeat(2) {
+ rule.runOnIdle {
+ counter = 1
+
+ childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ assertThat(counter).isEqualTo(3)
+ counter = 1
+
+ childDispatcher.dispatchPostScroll(
+ scrollOffset,
+ scrollLeftOffset,
+ NestedScrollSource.Drag
+ )
+ assertThat(counter).isEqualTo(3)
+ counter = 1
+
+ childDispatcher.dispatchPreFling(preFling)
+ assertThat(counter).isEqualTo(3)
+ counter = 1
+
+ childDispatcher.dispatchPostFling(postFlingConsumed, postFlingLeft)
+ assertThat(counter).isEqualTo(3)
+ counter = 1
+
+ isConnection1Parent.value = !isConnection1Parent.value
+ }
+ }
+ }
+
+ @Test
+ fun nestedScroll_hierarchyDispatch_runtimeSwapChange_orderTest() {
+ val preScrollReturn = Offset(60f, 30f)
+ val preFlingReturn = Velocity(Offset(154f, 56f))
+ var counter = 0
+
+ val isConnection1Parent = mutableStateOf(true)
+ val childConnection = object : NestedScrollConnection {}
+ val connection1 = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ assertThat(counter).isEqualTo(if (isConnection1Parent.value) 1 else 2)
+ counter++
+ return preScrollReturn
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ assertThat(counter).isEqualTo(if (isConnection1Parent.value) 2 else 1)
+ counter++
+ return Offset.Zero
+ }
+
+ override fun onPreFling(available: Velocity): Velocity {
+ assertThat(counter).isEqualTo(if (isConnection1Parent.value) 1 else 2)
+ counter++
+ return preFlingReturn
+ }
+
+ override fun onPostFling(
+ consumed: Velocity,
+ available: Velocity,
+ onFinished: (Velocity) -> Unit
+ ) {
+ assertThat(counter).isEqualTo(if (isConnection1Parent.value) 2 else 1)
+ counter++
+ onFinished.invoke(Velocity.Zero)
+ }
+ }
+ val connection2 = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ assertThat(counter).isEqualTo(if (isConnection1Parent.value) 2 else 1)
+ counter++
+ return preScrollReturn
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ assertThat(counter).isEqualTo(if (isConnection1Parent.value) 1 else 2)
+ counter++
+ return Offset.Zero
+ }
+
+ override fun onPreFling(available: Velocity): Velocity {
+ assertThat(counter).isEqualTo(if (isConnection1Parent.value) 2 else 1)
+ counter++
+ return preFlingReturn
+ }
+
+ override fun onPostFling(
+ consumed: Velocity,
+ available: Velocity,
+ onFinished: (Velocity) -> Unit
+ ) {
+ assertThat(counter).isEqualTo(if (isConnection1Parent.value) 1 else 2)
+ counter++
+ onFinished.invoke(Velocity.Zero)
+ }
+ }
+ val childDispatcher = NestedScrollDispatcher()
+ rule.setContent {
+ val outerBoxConnection = if (isConnection1Parent.value) connection1 else connection2
+ val innerBoxConnection = if (isConnection1Parent.value) connection2 else connection1
+ Box(Modifier.size(100.dp).nestedScroll(outerBoxConnection)) {
+ Box(Modifier.size(100.dp).nestedScroll(innerBoxConnection)) {
+ Box(Modifier.nestedScroll(childConnection, childDispatcher))
+ }
+ }
+ }
+
+ repeat(2) {
+ rule.runOnIdle {
+ counter = 1
+
+ childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ assertThat(counter).isEqualTo(3)
+ counter = 1
+
+ childDispatcher.dispatchPostScroll(
+ scrollOffset,
+ scrollLeftOffset,
+ NestedScrollSource.Drag
+ )
+ assertThat(counter).isEqualTo(3)
+ counter = 1
+
+ childDispatcher.dispatchPreFling(preFling)
+ assertThat(counter).isEqualTo(3)
+ counter = 1
+
+ childDispatcher.dispatchPostFling(postFlingConsumed, postFlingLeft)
+ assertThat(counter).isEqualTo(3)
+ counter = 1
+
+ isConnection1Parent.value = !isConnection1Parent.value
+ }
+ }
+ }
+
+ // helper functions
+
+ private fun testMiddleParentAdditionRemoval(
+ content: @Composable (root: Modifier, middle: Modifier, child: Modifier) -> Unit
+ ) {
+ val rootParentPreConsumed = Offset(60f, 30f)
+ val parentToRemovePreConsumed = Offset(21f, 44f)
+
+ val emitNewParent = mutableStateOf(true)
+ val childConnection = object : NestedScrollConnection {}
+ val rootParent = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ return rootParentPreConsumed
+ }
+ }
+ val parentToRemove = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ if (!emitNewParent.value) {
+ assertWithMessage("Shouldn't be called when not emitted").fail()
+ }
+ return parentToRemovePreConsumed
+ }
+ }
+ val childDispatcher = NestedScrollDispatcher()
+ rule.setContent {
+ val maybeNestedScroll =
+ if (emitNewParent.value) Modifier.nestedScroll(parentToRemove) else Modifier
+ content.invoke(
+ Modifier.nestedScroll(rootParent),
+ maybeNestedScroll,
+ Modifier.nestedScroll(childConnection, childDispatcher)
+ )
+ }
+
+ rule.runOnIdle {
+ val res =
+ childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ assertThat(res).isEqualTo(rootParentPreConsumed + parentToRemovePreConsumed)
+
+ emitNewParent.value = false
+ }
+
+ rule.runOnIdle {
+ val res =
+ childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ assertThat(res).isEqualTo(rootParentPreConsumed)
+
+ emitNewParent.value = true
+ }
+
+ rule.runOnIdle {
+ val res =
+ childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ assertThat(res).isEqualTo(rootParentPreConsumed + parentToRemovePreConsumed)
+ }
+ }
+
+ private fun testRootParentAdditionRemoval(
+ content: @Composable (root: Modifier, child: Modifier) -> Unit
+ ) {
+ val preScrollReturn = Offset(60f, 30f)
+
+ val emitParentNestedScroll = mutableStateOf(true)
+ val childConnection = object : NestedScrollConnection {}
+ val parent = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ return preScrollReturn
+ }
+ }
+ val childDispatcher = NestedScrollDispatcher()
+ rule.setContent {
+ val maybeNestedScroll =
+ if (emitParentNestedScroll.value) Modifier.nestedScroll(parent) else Modifier
+ content.invoke(
+ maybeNestedScroll,
+ Modifier.nestedScroll(childConnection, childDispatcher)
+ )
+ }
+
+ rule.runOnIdle {
+ val res = childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ assertThat(res).isEqualTo(preScrollReturn)
+
+ emitParentNestedScroll.value = false
+ }
+
+ rule.runOnIdle {
+ val res = childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ assertThat(res).isEqualTo(Offset.Zero)
+ emitParentNestedScroll.value = true
+ }
+
+ rule.runOnIdle {
+ val res = childDispatcher.dispatchPreScroll(preScrollOffset, NestedScrollSource.Drag)
+ assertThat(res).isEqualTo(preScrollReturn)
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/AndroidProcessKeyInputTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/AndroidProcessKeyInputTest.kt
index 8ed2323..58a9977 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/AndroidProcessKeyInputTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/AndroidProcessKeyInputTest.kt
@@ -23,7 +23,6 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.setFocusableContent
import androidx.compose.ui.focusRequester
@@ -46,10 +45,6 @@
*/
@SmallTest
@RunWith(Parameterized::class)
-@OptIn(
- ExperimentalFocus::class,
- ExperimentalKeyInput::class
-)
class AndroidProcessKeyInputTest(val keyEventAction: Int) {
@get:Rule
val rule = createComposeRule()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/KeyInputUtil.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/KeyInputUtil.kt
index da95e3d..fdc0120 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/KeyInputUtil.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/KeyInputUtil.kt
@@ -25,7 +25,6 @@
* The [KeyEvent] is usually created by the system. This function creates an instance of
* [KeyEvent] that can be used in tests.
*/
-@OptIn(ExperimentalKeyInput::class)
fun keyEvent(key: Key, keyEventType: KeyEventType, androidMetaKeys: Int = 0): KeyEvent {
val action = when (keyEventType) {
KeyEventType.KeyDown -> ACTION_DOWN
@@ -40,7 +39,6 @@
* [KeyEventAndroid] inline classes do not allow
* overriding the equals() function. So we use this util function to compare KeyEvents.
*/
-@OptIn(ExperimentalKeyInput::class)
fun KeyEvent.assertEqualTo(expected: KeyEvent) {
Truth.assertThat(key).isEqualTo(expected.key)
Truth.assertThat(type).isEqualTo(expected.type)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/MetaKeyTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/MetaKeyTest.kt
index d5ca9df..ded752b 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/MetaKeyTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/MetaKeyTest.kt
@@ -28,7 +28,6 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalKeyInput::class)
class MetaKeyTest {
@Test
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/ProcessKeyInputTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/ProcessKeyInputTest.kt
index a95401c..2b77b0c 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/ProcessKeyInputTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/ProcessKeyInputTest.kt
@@ -19,7 +19,6 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.setFocusableContent
import androidx.compose.ui.focusRequester
@@ -38,10 +37,6 @@
@Suppress("DEPRECATION")
@MediumTest
@RunWith(AndroidJUnit4::class)
-@OptIn(
- ExperimentalFocus::class,
- ExperimentalKeyInput::class
-)
class ProcessKeyInputTest {
@get:Rule
val rule = createComposeRule()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
index f9e3ac6..e209584 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
@@ -16,15 +16,14 @@
package androidx.compose.ui.input.pointer
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.Autofill
import androidx.compose.ui.autofill.AutofillTree
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.hapticfeedback.HapticFeedback
-import androidx.compose.ui.input.key.ExperimentalKeyInput
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
@@ -3131,20 +3130,14 @@
targetRoot: LayoutNode = LayoutNode()
): Owner = MockOwner(position, targetRoot)
-@OptIn(
- ExperimentalFocus::class,
- InternalCoreApi::class
-)
+@OptIn(ExperimentalComposeUiApi::class, InternalCoreApi::class)
private class MockOwner(
private val position: IntOffset,
private val targetRoot: LayoutNode
) : Owner {
override fun calculatePosition(): IntOffset = position
override fun requestFocus(): Boolean = false
-
- @ExperimentalKeyInput
override fun sendKeyEvent(keyEvent: KeyEvent): Boolean = false
-
override val root: LayoutNode
get() = targetRoot
override val hapticFeedBack: HapticFeedback
@@ -3181,8 +3174,6 @@
override fun onRequestRelayout(layoutNode: LayoutNode) {
}
- override val hasPendingMeasureOrLayout = false
-
override fun onAttach(node: LayoutNode) {
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
index 4433814..7ca9b32 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
@@ -18,7 +18,6 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.gesture.ExperimentalPointerInput
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.ValueElement
import androidx.compose.ui.platform.ViewConfiguration
@@ -48,7 +47,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalPointerInput::class, ExperimentalCoroutinesApi::class)
+@OptIn(ExperimentalCoroutinesApi::class)
class SuspendingPointerInputFilterTest {
@After
fun after() {
@@ -235,7 +234,6 @@
private fun PointerInputChange.toPointerEvent() = PointerEvent(listOf(this))
-@ExperimentalPointerInput
private val PointerEvent.firstChange get() = changes.first()
private class PointerInputChangeEmitter(id: Int = 0) {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/HotReloadTests.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/HotReloadTests.kt
index 6fb6d26..d20954b 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/HotReloadTests.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/HotReloadTests.kt
@@ -30,7 +30,7 @@
import androidx.compose.ui.platform.AmbientContext
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.accessibilityLabel
+import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.assertLabelEquals
@@ -125,7 +125,7 @@
var value = "First value"
@Composable fun semanticsNode(text: String, id: Int) {
- Box(Modifier.testTag("text$id").semantics { accessibilityLabel = text }) {
+ Box(Modifier.testTag("text$id").semantics { contentDescription = text }) {
}
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
index b06582c..04dca9c 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
@@ -63,7 +63,6 @@
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.node.Owner
import androidx.compose.ui.node.Ref
-import androidx.compose.ui.node.isAttached
import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.captureToImage
@@ -619,7 +618,7 @@
assertNotNull(innerAndroidComposeView)
assertTrue(innerAndroidComposeView!!.isAttachedToWindow)
assertNotNull(node)
- assertTrue(node!!.isAttached())
+ assertTrue(node!!.isAttached)
rule.runOnIdle { composeContent = false }
@@ -627,7 +626,7 @@
rule.runOnIdle {
assertFalse(innerAndroidComposeView!!.isAttachedToWindow)
// the node stays attached after the compose view is detached
- assertTrue(node!!.isAttached())
+ assertTrue(node!!.isAttached)
}
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowManagerAmbientTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowManagerAmbientTest.kt
index f189a3d..d50dc78 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowManagerAmbientTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowManagerAmbientTest.kt
@@ -18,7 +18,6 @@
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.Popup
@@ -33,7 +32,6 @@
import java.util.concurrent.TimeUnit.SECONDS
@MediumTest
-@OptIn(ExperimentalFocus::class)
@RunWith(AndroidJUnit4::class)
class WindowManagerAmbientTest {
@get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
index 6dbf1c4..97c6889 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
@@ -19,7 +19,6 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.InspectableValue
@@ -34,9 +33,9 @@
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.assertValueEquals
import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onAllNodesWithLabel
+import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onAllNodesWithText
-import androidx.compose.ui.test.onNodeWithLabel
+import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.text.AnnotatedString
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -85,7 +84,7 @@
val recomposeForcer = mutableStateOf(0)
rule.setContent {
recomposeForcer.value
- CountingLayout(Modifier.semantics { accessibilityLabel = "label" }, layoutCounter)
+ CountingLayout(Modifier.semantics { contentDescription = "label" }, layoutCounter)
}
rule.runOnIdle { assertEquals(1, layoutCounter.count) }
@@ -105,13 +104,13 @@
rule.setContent {
SimpleTestLayout(
Modifier.testTag(TestTag)
- .semantics(mergeDescendants = true) { accessibilityLabel = root }
+ .semantics(mergeDescendants = true) { contentDescription = root }
) {
- SimpleTestLayout(Modifier.semantics { accessibilityLabel = child1 }) {
- SimpleTestLayout(Modifier.semantics { accessibilityLabel = grandchild1 }) { }
- SimpleTestLayout(Modifier.semantics { accessibilityLabel = grandchild2 }) { }
+ SimpleTestLayout(Modifier.semantics { contentDescription = child1 }) {
+ SimpleTestLayout(Modifier.semantics { contentDescription = grandchild1 }) { }
+ SimpleTestLayout(Modifier.semantics { contentDescription = grandchild2 }) { }
}
- SimpleTestLayout(Modifier.semantics { accessibilityLabel = child2 }) { }
+ SimpleTestLayout(Modifier.semantics { contentDescription = child2 }) { }
}
}
@@ -128,9 +127,9 @@
val label2 = "bar"
rule.setContent {
SimpleTestLayout(Modifier.semantics(mergeDescendants = true) {}.testTag(tag1)) {
- SimpleTestLayout(Modifier.semantics { accessibilityLabel = label1 }) { }
+ SimpleTestLayout(Modifier.semantics { contentDescription = label1 }) { }
SimpleTestLayout(Modifier.semantics(mergeDescendants = true) {}.testTag(tag2)) {
- SimpleTestLayout(Modifier.semantics { accessibilityLabel = label2 }) { }
+ SimpleTestLayout(Modifier.semantics { contentDescription = label2 }) { }
}
}
}
@@ -148,12 +147,12 @@
val label3 = "baz"
rule.setContent {
SimpleTestLayout(Modifier.semantics(mergeDescendants = true) {}.testTag(tag1)) {
- SimpleTestLayout(Modifier.semantics { accessibilityLabel = label1 }) { }
+ SimpleTestLayout(Modifier.semantics { contentDescription = label1 }) { }
SimpleTestLayout(Modifier.clearAndSetSemantics {}) {
- SimpleTestLayout(Modifier.semantics { accessibilityLabel = label2 }) { }
+ SimpleTestLayout(Modifier.semantics { contentDescription = label2 }) { }
}
- SimpleTestLayout(Modifier.clearAndSetSemantics { accessibilityLabel = label3 }) {
- SimpleTestLayout(Modifier.semantics { accessibilityLabel = label2 }) { }
+ SimpleTestLayout(Modifier.clearAndSetSemantics { contentDescription = label3 }) {
+ SimpleTestLayout(Modifier.semantics { contentDescription = label2 }) { }
}
SimpleTestLayout(
Modifier.semantics(mergeDescendants = true) {}.testTag(tag2)
@@ -174,7 +173,7 @@
rule.setContent {
SimpleTestLayout(Modifier.semantics(mergeDescendants = true) {}.testTag(TestTag)) {
if (showSubtree.value) {
- SimpleTestLayout(Modifier.semantics { accessibilityLabel = label }) { }
+ SimpleTestLayout(Modifier.semantics { contentDescription = label }) { }
}
}
}
@@ -184,7 +183,7 @@
rule.runOnIdle { showSubtree.value = false }
rule.onNodeWithTag(TestTag)
- .assertDoesNotHaveProperty(SemanticsProperties.AccessibilityLabel)
+ .assertDoesNotHaveProperty(SemanticsProperties.ContentDescription)
rule.onAllNodesWithText(label).assertCountEquals(0)
}
@@ -196,16 +195,16 @@
val showNewNode = mutableStateOf(false)
rule.setContent {
SimpleTestLayout(Modifier.semantics(mergeDescendants = true) {}.testTag(TestTag)) {
- SimpleTestLayout(Modifier.semantics { accessibilityLabel = label }) { }
+ SimpleTestLayout(Modifier.semantics { contentDescription = label }) { }
if (showNewNode.value) {
- SimpleTestLayout(Modifier.semantics { accessibilityValue = value }) { }
+ SimpleTestLayout(Modifier.semantics { stateDescription = value }) { }
}
}
}
rule.onNodeWithTag(TestTag)
.assertLabelEquals(label)
- .assertDoesNotHaveProperty(SemanticsProperties.AccessibilityValue)
+ .assertDoesNotHaveProperty(SemanticsProperties.StateDescription)
rule.runOnIdle { showNewNode.value = true }
@@ -221,18 +220,18 @@
rule.setContent {
SimpleTestLayout(Modifier.testTag(TestTag)) {
if (showSubtree.value) {
- SimpleTestLayout(Modifier.semantics { accessibilityLabel = label }) { }
+ SimpleTestLayout(Modifier.semantics { contentDescription = label }) { }
}
}
}
- rule.onAllNodesWithLabel(label).assertCountEquals(1)
+ rule.onAllNodesWithContentDescription(label).assertCountEquals(1)
rule.runOnIdle {
showSubtree.value = false
}
- rule.onAllNodesWithLabel(label).assertCountEquals(0)
+ rule.onAllNodesWithContentDescription(label).assertCountEquals(0)
}
@Test
@@ -243,7 +242,7 @@
rule.setContent {
SimpleTestLayout(
Modifier.testTag(TestTag).semantics {
- accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
+ contentDescription = if (isAfter.value) afterLabel else beforeLabel
}
) {}
}
@@ -265,7 +264,7 @@
SimpleTestLayout(Modifier.testTag("don't care")) {
SimpleTestLayout(
Modifier.testTag(TestTag).semantics {
- accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
+ contentDescription = if (isAfter.value) afterLabel else beforeLabel
}
) {}
}
@@ -289,7 +288,7 @@
SimpleTestLayout {
SimpleTestLayout(
Modifier.testTag(TestTag).semantics {
- accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
+ contentDescription = if (isAfter.value) afterLabel else beforeLabel
}
) {}
}
@@ -313,7 +312,7 @@
SimpleTestLayout(Modifier.testTag(TestTag).semantics(mergeDescendants = true) {}) {
SimpleTestLayout(
Modifier.semantics {
- accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
+ contentDescription = if (isAfter.value) afterLabel else beforeLabel
}
) {}
}
@@ -332,14 +331,14 @@
rule.setContent {
SimpleTestLayout(Modifier.testTag(TestTag)) {
SimpleTestLayout(Modifier.semantics(mergeDescendants = true) {}) {
- SimpleTestLayout(Modifier.semantics { accessibilityLabel = label }) { }
+ SimpleTestLayout(Modifier.semantics { contentDescription = label }) { }
}
}
}
rule.onNodeWithTag(TestTag)
- .assertDoesNotHaveProperty(SemanticsProperties.AccessibilityLabel)
- rule.onNodeWithLabel(label) // assert exists
+ .assertDoesNotHaveProperty(SemanticsProperties.ContentDescription)
+ rule.onNodeWithContentDescription(label) // assert exists
}
@Test
@@ -354,7 +353,7 @@
rule.setContent {
SimpleTestLayout(
Modifier.testTag(TestTag).semantics {
- accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
+ contentDescription = if (isAfter.value) afterLabel else beforeLabel
}
) {
SimpleTestLayout(Modifier.semantics { }) { }
@@ -385,7 +384,7 @@
rule.setContent {
SimpleTestLayout(
Modifier.testTag(TestTag).semantics {
- accessibilityLabel = if (isAfter.value) afterLabel else beforeLabel
+ contentDescription = if (isAfter.value) afterLabel else beforeLabel
onClick(
action = {
if (isAfter.value) afterAction() else beforeAction()
@@ -395,7 +394,7 @@
}
) {
SimpleTestLayout {
- remember { nodeCount++ }
+ nodeCount++
}
}
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/EditTextInteropTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/EditTextInteropTest.kt
index 03c512c..5b07d5e 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/EditTextInteropTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/EditTextInteropTest.kt
@@ -20,7 +20,6 @@
import android.view.View
import android.widget.EditText
import androidx.activity.ComponentActivity
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.platform.AmbientView
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.text.InternalTextApi
@@ -32,7 +31,7 @@
import org.junit.runner.RunWith
@MediumTest
-@OptIn(ExperimentalFocus::class, InternalTextApi::class)
+@OptIn(InternalTextApi::class)
@RunWith(AndroidJUnit4::class)
class EditTextInteropTest {
@get:Rule
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogSecureFlagTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogSecureFlagTest.kt
index df3bb5e..dcdc42e 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogSecureFlagTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/DialogSecureFlagTest.kt
@@ -124,7 +124,7 @@
private fun isSecureFlagEnabledForDialog(): Boolean {
val owner = rule
.onNode(isDialog())
- .fetchSemanticsNode("").layoutNode.owner as View
+ .fetchSemanticsNode("").owner as View
return (owner.rootView.layoutParams as WindowManager.LayoutParams).flags and
WindowManager.LayoutParams.FLAG_SECURE != 0
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.kt
index 2b9b545..9c57132 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.kt
@@ -24,6 +24,7 @@
import android.view.autofill.AutofillManager
import android.view.autofill.AutofillValue
import androidx.annotation.RequiresApi
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.graphics.toAndroidRect
/**
@@ -32,6 +33,7 @@
* @param view The parent compose view.
* @param autofillTree The autofill tree. This will be replaced by a semantic tree (b/138604305).
*/
+@ExperimentalComposeUiApi
@RequiresApi(Build.VERSION_CODES.O)
internal class AndroidAutofill(val view: View, val autofillTree: AutofillTree) : Autofill {
@@ -63,6 +65,7 @@
*
* This function populates the view structure using the information in the { AutofillTree }.
*/
+@ExperimentalComposeUiApi
@RequiresApi(Build.VERSION_CODES.O)
internal fun AndroidAutofill.populateViewStructure(root: ViewStructure) {
@@ -96,6 +99,7 @@
/**
* Triggers onFill() in response to a request from the autofill framework.
*/
+@ExperimentalComposeUiApi
@RequiresApi(Build.VERSION_CODES.O)
internal fun AndroidAutofill.performAutofill(values: SparseArray<AutofillValue>) {
for (index in 0 until values.size()) {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillDebugUtils.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillDebugUtils.kt
index 6d50b7e..839587ce 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillDebugUtils.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillDebugUtils.kt
@@ -21,6 +21,7 @@
import android.view.View
import android.view.autofill.AutofillManager
import androidx.annotation.RequiresApi
+import androidx.compose.ui.ExperimentalComposeUiApi
/**
* Autofill Manager callback.
@@ -58,6 +59,7 @@
/**
* Registers the autofill debug callback.
*/
+@ExperimentalComposeUiApi
@RequiresApi(Build.VERSION_CODES.O)
internal fun AndroidAutofill.registerCallback() {
autofillManager.registerCallback(AutofillCallback)
@@ -66,6 +68,7 @@
/**
* Unregisters the autofill debug callback.
*/
+@ExperimentalComposeUiApi
@RequiresApi(Build.VERSION_CODES.O)
internal fun AndroidAutofill.unregisterCallback() {
autofillManager.unregisterCallback(AutofillCallback)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillType.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillType.kt
index 5e057b5..94a5b9a 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillType.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillType.kt
@@ -52,6 +52,7 @@
import androidx.autofill.HintConstants.AUTOFILL_HINT_POSTAL_CODE
import androidx.autofill.HintConstants.AUTOFILL_HINT_SMS_OTP
import androidx.autofill.HintConstants.AUTOFILL_HINT_USERNAME
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.autofill.AutofillType.AddressAuxiliaryDetails
import androidx.compose.ui.autofill.AutofillType.AddressCountry
import androidx.compose.ui.autofill.AutofillType.AddressLocality
@@ -93,6 +94,7 @@
* Gets the Android specific [AutofillHint][android.view.ViewStructure.setAutofillHints]
* corresponding to the current [AutofillType].
*/
+@ExperimentalComposeUiApi
internal val AutofillType.androidType: String
get() {
val androidAutofillType = androidAutofillTypes[this]
@@ -103,6 +105,7 @@
/**
* Maps each [AutofillType] to one of the autofill hints in [androidx.autofill.HintConstants]
*/
+@ExperimentalComposeUiApi
private val androidAutofillTypes: HashMap<AutofillType, String> = hashMapOf(
EmailAddress to AUTOFILL_HINT_EMAIL_ADDRESS,
Username to AUTOFILL_HINT_USERNAME,
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/key/KeyEventAndroid.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/key/KeyEventAndroid.kt
index b5a86d7..5aa92c0 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/key/KeyEventAndroid.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/key/KeyEventAndroid.kt
@@ -26,7 +26,6 @@
import androidx.compose.ui.input.key.KeyEventType.Unknown
import android.view.KeyEvent as AndroidKeyEvent
-@OptIn(ExperimentalKeyInput::class)
internal inline class KeyEventAndroid(val keyEvent: AndroidKeyEvent) : KeyEvent {
override val key: Key
@@ -59,7 +58,6 @@
}
@Suppress("DEPRECATION")
-@OptIn(ExperimentalKeyInput::class)
internal inline class AltAndroid(val keyEvent: AndroidKeyEvent) : Alt {
override val isLeftAltPressed
get() = (keyEvent.metaState and META_ALT_LEFT_ON) != 0
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.kt
index 1d704f3..92a0968 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.kt
@@ -19,111 +19,73 @@
import android.view.View
import android.view.ViewGroup
-import androidx.compose.runtime.Applier
+import androidx.compose.runtime.AbstractApplier
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.ui.platform.AndroidComposeView
import androidx.compose.ui.viewinterop.AndroidViewHolder
import androidx.compose.ui.viewinterop.InternalInteropApi
import androidx.compose.ui.viewinterop.ViewBlockHolder
-// TODO: evaluate if this class is necessary or not
-private class Stack<T> {
- private val backing = ArrayList<T>()
-
- val size: Int get() = backing.size
-
- fun push(value: T) = backing.add(value)
- fun pop(): T = backing.removeAt(size - 1)
- fun peek(): T = backing[size - 1]
- fun isEmpty() = backing.isEmpty()
- fun isNotEmpty() = !isEmpty()
- fun clear() = backing.clear()
-}
-
-class UiApplier(
- private val root: Any
-) : Applier<Any> {
- private val stack = Stack<Any>()
- private data class PendingInsert(val index: Int, val instance: Any)
- // TODO(b/159073250): remove
- private val pendingInserts = Stack<PendingInsert>()
-
+class UiApplier(root: Any) : AbstractApplier<Any>(root) {
private fun invalidNode(node: Any): Nothing =
error("Unsupported node type ${node.javaClass.simpleName}")
- override var current: Any = root
- private set
-
- override fun down(node: Any) {
- stack.push(current)
- current = node
- }
-
override fun up() {
val instance = current
- val parent = stack.pop()
- current = parent
- // TODO(lmr): We should strongly consider removing this ViewAdapter concept
+ super.up()
+ val parent = current
+ if (parent is ViewGroup && instance is View) {
+ instance.getViewAdapterIfExists()?.didUpdate(instance, parent)
+ }
+ }
+
+ override fun insertTopDown(index: Int, instance: Any) {
+ // Ignored. Insert is performed in [insertBottomUp] to build the tree bottom-up to avoid
+ // duplicate notification when the child nodes enter the tree.
+ }
+
+ override fun insertBottomUp(index: Int, instance: Any) {
val adapter = when (instance) {
is View -> instance.getViewAdapterIfExists()
else -> null
}
- if (pendingInserts.isNotEmpty()) {
- val pendingInsert = pendingInserts.peek()
- if (pendingInsert.instance == instance) {
- val index = pendingInsert.index
- pendingInserts.pop()
- when (parent) {
- is ViewGroup ->
- when (instance) {
- is View -> {
- adapter?.willInsert(instance, parent)
- parent.addView(instance, index)
- adapter?.didInsert(instance, parent)
- }
- is LayoutNode -> {
- val composeView = AndroidComposeView(parent.context)
- parent.addView(composeView, index)
- composeView.root.insertAt(0, instance)
- }
- else -> invalidNode(instance)
- }
- is LayoutNode ->
- when (instance) {
- is View -> {
- // Wrap the instance in an AndroidViewHolder, unless the instance
- // itself is already one.
- @OptIn(InternalInteropApi::class)
- val androidViewHolder =
- if (instance is AndroidViewHolder) {
- instance
- } else {
- ViewBlockHolder<View>(instance.context).apply {
- view = instance
- }
- }
-
- parent.insertAt(index, androidViewHolder.toLayoutNode())
- }
- is LayoutNode -> parent.insertAt(index, instance)
- else -> invalidNode(instance)
- }
- else -> invalidNode(parent)
+ when (val parent = current) {
+ is ViewGroup ->
+ when (instance) {
+ is View -> {
+ adapter?.willInsert(instance, parent)
+ parent.addView(instance, index)
+ adapter?.didInsert(instance, parent)
+ }
+ is LayoutNode -> {
+ val composeView = AndroidComposeView(parent.context)
+ parent.addView(composeView, index)
+ composeView.root.insertAt(0, instance)
+ }
+ else -> invalidNode(instance)
}
- return
- }
- }
- if (parent is ViewGroup)
- adapter?.didUpdate(instance as View, parent)
- }
+ is LayoutNode ->
+ when (instance) {
+ is View -> {
+ // Wrap the instance in an AndroidViewHolder, unless the instance
+ // itself is already one.
+ @OptIn(InternalInteropApi::class)
+ val androidViewHolder =
+ if (instance is AndroidViewHolder) {
+ instance
+ } else {
+ ViewBlockHolder<View>(instance.context).apply {
+ view = instance
+ }
+ }
- override fun insert(index: Int, instance: Any) {
- pendingInserts.push(
- PendingInsert(
- index,
- instance
- )
- )
+ parent.insertAt(index, androidViewHolder.toLayoutNode())
+ }
+ is LayoutNode -> parent.insertAt(index, instance)
+ else -> invalidNode(instance)
+ }
+ else -> invalidNode(parent)
+ }
}
override fun remove(index: Int, count: Int) {
@@ -162,13 +124,31 @@
}
}
- override fun clear() {
- stack.clear()
- current = root
+ override fun onClear() {
when (root) {
is ViewGroup -> root.removeAllViews()
is LayoutNode -> root.removeAll()
else -> invalidNode(root)
}
}
+
+ override fun onEndChanges() {
+ super.onEndChanges()
+ if (root is ViewGroup) {
+ clearInvalidObservations(root)
+ } else if (root is LayoutNode) {
+ (root.owner as? AndroidComposeView)?.clearInvalidObservations()
+ }
+ }
+
+ private fun clearInvalidObservations(viewGroup: ViewGroup) {
+ for (i in 0 until viewGroup.childCount) {
+ val child = viewGroup.getChildAt(i)
+ if (child is AndroidComposeView) {
+ child.clearInvalidObservations()
+ } else if (child is ViewGroup) {
+ clearInvalidObservations(child)
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/ViewInterop.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/ViewInterop.kt
index 4cc5cf7..45698e4 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/ViewInterop.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/ViewInterop.kt
@@ -29,7 +29,7 @@
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.platform.AndroidComposeView
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastForEach
@@ -106,7 +106,7 @@
.pointerInteropFilter(this)
.drawBehind {
drawIntoCanvas { canvas ->
- (layoutNode.owner as? AndroidOwner)
+ (layoutNode.owner as? AndroidComposeView)
?.drawAndroidView(this@toLayoutNode, canvas.nativeCanvas)
}
}.onGloballyPositioned {
@@ -122,11 +122,11 @@
var viewRemovedOnDetach: View? = null
layoutNode.onAttach = { owner ->
- (owner as? AndroidOwner)?.addAndroidView(this, layoutNode)
+ (owner as? AndroidComposeView)?.addAndroidView(this, layoutNode)
if (viewRemovedOnDetach != null) view = viewRemovedOnDetach
}
layoutNode.onDetach = { owner ->
- (owner as? AndroidOwner)?.removeAndroidView(this)
+ (owner as? AndroidComposeView)?.removeAndroidView(this)
viewRemovedOnDetach = view
view = null
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AccessibilityIterators.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AccessibilityIterators.kt
index ed51935..a102b98 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AccessibilityIterators.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AccessibilityIterators.kt
@@ -28,7 +28,7 @@
* This class contains the implementation of text segment iterators
* for accessibility support.
*
- * Note: We want to be able to iterator over [SemanticsProperties.AccessibilityLabel] of any
+ * Note: We want to be able to iterator over [SemanticsProperties.ContentDescription] of any
* component.
*/
internal class AccessibilityIterators {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidAmbients.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidAmbients.kt
index 9e28215..083e828 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidAmbients.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidAmbients.kt
@@ -34,6 +34,7 @@
import androidx.compose.runtime.savedinstancestate.AmbientUiSavedStateRegistry
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticAmbientOf
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelStoreOwner
@@ -133,9 +134,9 @@
val AmbientViewModelStoreOwner = staticAmbientOf<ViewModelStoreOwner>()
@Composable
-@OptIn(InternalAnimationApi::class)
-internal fun ProvideAndroidAmbients(owner: AndroidOwner, content: @Composable () -> Unit) {
- val view = owner.view
+@OptIn(ExperimentalComposeUiApi::class, InternalAnimationApi::class)
+internal fun ProvideAndroidAmbients(owner: AndroidComposeView, content: @Composable () -> Unit) {
+ val view = owner
val context = view.context
val scope = rememberCoroutineScope()
val rootAnimationClock = remember(scope) {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
index 3e63744..794fcbb 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
@@ -35,6 +35,7 @@
import android.view.inputmethod.InputConnection
import androidx.annotation.RequiresApi
import androidx.compose.runtime.ExperimentalComposeApi
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.AndroidAutofill
import androidx.compose.ui.autofill.Autofill
@@ -43,7 +44,6 @@
import androidx.compose.ui.autofill.populateViewStructure
import androidx.compose.ui.autofill.registerCallback
import androidx.compose.ui.autofill.unregisterCallback
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FOCUS_TAG
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.FocusManagerImpl
@@ -51,7 +51,6 @@
import androidx.compose.ui.graphics.CanvasHolder
import androidx.compose.ui.hapticfeedback.AndroidHapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedback
-import androidx.compose.ui.input.key.ExperimentalKeyInput
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventAndroid
import androidx.compose.ui.input.key.KeyInputModifier
@@ -64,6 +63,7 @@
import androidx.compose.ui.node.LayoutNode.UsageByParent
import androidx.compose.ui.node.MeasureAndLayoutDelegate
import androidx.compose.ui.node.OwnedLayer
+import androidx.compose.ui.node.Owner
import androidx.compose.ui.node.OwnerSnapshotObserver
import androidx.compose.ui.semantics.SemanticsModifierCore
import androidx.compose.ui.semantics.SemanticsOwner
@@ -80,20 +80,24 @@
import androidx.compose.ui.viewinterop.InternalInteropApi
import androidx.core.os.HandlerCompat
import androidx.core.view.ViewCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.ViewTreeLifecycleOwner
import androidx.lifecycle.ViewTreeViewModelStoreOwner
+import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.ViewTreeSavedStateRegistryOwner
import java.lang.reflect.Method
import android.view.KeyEvent as AndroidKeyEvent
-@SuppressLint("ViewConstructor")
+@SuppressLint("ViewConstructor", "VisibleForTests")
@OptIn(
ExperimentalComposeApi::class,
- ExperimentalFocus::class,
- ExperimentalKeyInput::class,
+ ExperimentalComposeUiApi::class,
)
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
-internal class AndroidComposeView(context: Context) : ViewGroup(context), AndroidOwner {
+internal class AndroidComposeView(context: Context) :
+ ViewGroup(context), Owner, ViewRootForTest {
override val view: View = this
@@ -145,10 +149,12 @@
// TODO(mount): reinstate when coroutines are supported by IR compiler
// private val ownerScope = CoroutineScope(Dispatchers.Main.immediate + Job())
- // Used for updating the ConfigurationAmbient when configuration changes - consume the
- // configuration ambient instead of changing this observer if you are writing a component that
- // adapts to configuration changes.
- override var configurationChangeObserver: (Configuration) -> Unit = {}
+ /**
+ * Used for updating the ConfigurationAmbient when configuration changes - consume the
+ * configuration ambient instead of changing this observer if you are writing a component
+ * that adapts to configuration changes.
+ */
+ var configurationChangeObserver: (Configuration) -> Unit = {}
private val _autofill = if (autofillSupported()) AndroidAutofill(this, autofillTree) else null
@@ -173,13 +179,6 @@
@OptIn(InternalCoreApi::class)
override var showLayoutBounds = false
- private val clearInvalidObservations: Runnable = Runnable {
- if (observationClearRequested) {
- observationClearRequested = false
- snapshotObserver.clearInvalidObservations()
- }
- }
-
private var _androidViewsHandler: AndroidViewsHandler? = null
private val androidViewsHandler: AndroidViewsHandler
get() {
@@ -228,10 +227,14 @@
// so that we don't have to continue using try/catch after fails once.
private var isRenderNodeCompatible = true
- override var viewTreeOwners: AndroidOwner.ViewTreeOwners? = null
+ /**
+ * Current [ViewTreeOwners]. Use [setOnViewTreeOwnersAvailable] if you want to
+ * execute your code when the object will be created.
+ */
+ var viewTreeOwners: ViewTreeOwners? = null
private set
- private var onViewTreeOwnersAvailable: ((AndroidOwner.ViewTreeOwners) -> Unit)? = null
+ private var onViewTreeOwnersAvailable: ((ViewTreeOwners) -> Unit)? = null
// executed when the layout pass has been finished. as a result of it our view could be moved
// inside the window (we are interested not only in the event when our parent positioned us
@@ -282,7 +285,7 @@
isFocusableInTouchMode = true
clipChildren = false
ViewCompat.setAccessibilityDelegate(this, accessibilityDelegate)
- AndroidOwner.onAndroidOwnerCreatedCallback?.invoke(this)
+ ViewRootForTest.onViewCreatedCallback?.invoke(this)
root.attach(this)
}
@@ -324,27 +327,56 @@
}
fun requestClearInvalidObservations() {
- val handler = handler
- if (!observationClearRequested && handler != null) {
- observationClearRequested = true
- handler.postAtFrontOfQueue(clearInvalidObservations)
+ observationClearRequested = true
+ }
+
+ internal fun clearInvalidObservations() {
+ if (observationClearRequested) {
+ snapshotObserver.clearInvalidObservations()
+ observationClearRequested = false
+ }
+ val childAndroidViews = _androidViewsHandler
+ if (childAndroidViews != null) {
+ clearChildInvalidObservations(childAndroidViews)
}
}
+ private fun clearChildInvalidObservations(viewGroup: ViewGroup) {
+ for (i in 0 until viewGroup.childCount) {
+ val child = viewGroup.getChildAt(i)
+ if (child is AndroidComposeView) {
+ child.clearInvalidObservations()
+ } else if (child is ViewGroup) {
+ clearChildInvalidObservations(child)
+ }
+ }
+ }
+
+ /**
+ * Called to inform the owner that a new Android [View] was [attached][Owner.onAttach]
+ * to the hierarchy.
+ */
@OptIn(InternalInteropApi::class)
- override fun addAndroidView(view: AndroidViewHolder, layoutNode: LayoutNode) {
+ fun addAndroidView(view: AndroidViewHolder, layoutNode: LayoutNode) {
androidViewsHandler.layoutNode[view] = layoutNode
androidViewsHandler.addView(view)
}
+ /**
+ * Called to inform the owner that an Android [View] was [detached][Owner.onDetach]
+ * from the hierarchy.
+ */
@OptIn(InternalInteropApi::class)
- override fun removeAndroidView(view: AndroidViewHolder) {
+ fun removeAndroidView(view: AndroidViewHolder) {
androidViewsHandler.removeView(view)
androidViewsHandler.layoutNode.remove(view)
}
+ /**
+ * Called to ask the owner to draw a child Android [View] to [canvas].
+ */
@OptIn(InternalInteropApi::class)
- override fun drawAndroidView(view: AndroidViewHolder, canvas: android.graphics.Canvas) {
+ fun drawAndroidView(view: AndroidViewHolder, canvas: android.graphics.Canvas) {
androidViewsHandler.drawView(view, canvas)
}
@@ -503,7 +535,11 @@
}
}
- override fun setOnViewTreeOwnersAvailable(callback: (AndroidOwner.ViewTreeOwners) -> Unit) {
+ /**
+ * The callback to be executed when [viewTreeOwners] is created and not-null anymore.
+ * Note that this callback will be fired inline when it is already available
+ */
+ fun setOnViewTreeOwnersAvailable(callback: (ViewTreeOwners) -> Unit) {
val viewTreeOwners = viewTreeOwners
if (viewTreeOwners != null) {
callback(viewTreeOwners)
@@ -513,6 +549,18 @@
}
/**
+ * Android has an issue where calling showSoftwareKeyboard after calling
+ * hideSoftwareKeyboard, it results in keyboard flickering and sometimes the keyboard ends up
+ * being hidden even though the most recent call was to showKeyboard.
+ *
+ * This function starts a suspended function that listens for show/hide commands and only
+ * runs the latest command.
+ */
+ suspend fun keyboardVisibilityEventLoop() {
+ textInputServiceAndroid.keyboardVisibilityEventLoop()
+ }
+
+ /**
* Walks the entire LayoutNode sub-hierarchy and marks all nodes as needing measurement.
*/
private fun invalidateLayoutNodeMeasurement(node: LayoutNode) {
@@ -553,7 +601,7 @@
"Composed into the View which doesn't propagate" +
"ViewTreeSavedStateRegistryOwner!"
)
- val viewTreeOwners = AndroidOwner.ViewTreeOwners(
+ val viewTreeOwners = ViewTreeOwners(
lifecycleOwner = lifecycleOwner,
viewModelStoreOwner = viewModelStoreOwner,
savedStateRegistryOwner = savedStateRegistryOwner
@@ -575,14 +623,6 @@
}
viewTreeObserver.removeOnGlobalLayoutListener(globalLayoutListener)
viewTreeObserver.removeOnScrollChangedListener(scrollChangedListener)
-
- // In case of benchmarks, the handler callbacks will never get executed as benchmarks block
- // the main thread. However this callback holds references that point to this view which
- // effectively prevents it from being garbage collected in benchmarks.
- if (observationClearRequested) {
- observationClearRequested = false
- handler.removeCallbacks(clearInvalidObservations)
- }
}
override fun onProvideAutofillVirtualStructure(structure: ViewStructure?, flags: Int) {
@@ -645,6 +685,10 @@
return accessibilityDelegate.dispatchHoverEvent(event)
}
+ override val isLifecycleInResumedState: Boolean
+ get() = viewTreeOwners?.lifecycleOwner
+ ?.lifecycle?.currentState == Lifecycle.State.RESUMED
+
companion object {
private var systemPropertiesClass: Class<*>? = null
private var getBooleanMethod: Method? = null
@@ -665,6 +709,24 @@
false
}
}
+
+ /**
+ * Combines objects populated via ViewTree*Owner
+ */
+ class ViewTreeOwners(
+ /**
+ * The [LifecycleOwner] associated with this owner.
+ */
+ val lifecycleOwner: LifecycleOwner,
+ /**
+ * The [ViewModelStoreOwner] associated with this owner.
+ */
+ val viewModelStoreOwner: ViewModelStoreOwner,
+ /**
+ * The [SavedStateRegistryOwner] associated with this owner.
+ */
+ val savedStateRegistryOwner: SavedStateRegistryOwner
+ )
}
/**
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.kt
index 4dba953cc..dd24db3c 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.kt
@@ -236,9 +236,9 @@
ParcelSafeTextLength
)
info.stateDescription =
- semanticsNode.config.getOrNull(SemanticsProperties.AccessibilityValue)
+ semanticsNode.config.getOrNull(SemanticsProperties.StateDescription)
info.contentDescription =
- semanticsNode.config.getOrNull(SemanticsProperties.AccessibilityLabel)
+ semanticsNode.config.getOrNull(SemanticsProperties.ContentDescription)
// Note editable is not added to semantics properties api.
info.isEditable = semanticsNode.config.contains(SemanticsActions.SetText)
info.isEnabled = (semanticsNode.config.getOrNull(SemanticsProperties.Disabled) == null)
@@ -334,7 +334,7 @@
AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_CHARACTER or
AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_WORD or
AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_PARAGRAPH
- // We only traverse the text when accessibilityLabel is not set.
+ // We only traverse the text when contentDescription is not set.
if (info.contentDescription.isNullOrEmpty() &&
semanticsNode.config.contains(SemanticsActions.GetTextLayoutResult)
) {
@@ -1186,13 +1186,13 @@
continue
}
when (entry.key) {
- SemanticsProperties.AccessibilityValue ->
+ SemanticsProperties.StateDescription ->
sendEventForVirtualView(
semanticsNodeIdToAccessibilityVirtualNodeId(id),
AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
AccessibilityEvent.CONTENT_CHANGE_TYPE_STATE_DESCRIPTION
)
- SemanticsProperties.AccessibilityLabel ->
+ SemanticsProperties.ContentDescription ->
sendEventForVirtualView(
semanticsNodeIdToAccessibilityVirtualNodeId(id),
AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
@@ -1489,8 +1489,8 @@
}
private fun getAccessibilitySelectionStart(node: SemanticsNode): Int {
- // If there is AccessibilityLabel, it will be used instead of text during traversal.
- if (!node.config.contains(SemanticsProperties.AccessibilityLabel) &&
+ // If there is ContentDescription, it will be used instead of text during traversal.
+ if (!node.config.contains(SemanticsProperties.ContentDescription) &&
node.config.contains(SemanticsProperties.TextSelectionRange)
) {
return node.config[SemanticsProperties.TextSelectionRange].start
@@ -1499,8 +1499,8 @@
}
private fun getAccessibilitySelectionEnd(node: SemanticsNode): Int {
- // If there is AccessibilityLabel, it will be used instead of text during traversal.
- if (!node.config.contains(SemanticsProperties.AccessibilityLabel) &&
+ // If there is ContentDescription, it will be used instead of text during traversal.
+ if (!node.config.contains(SemanticsProperties.ContentDescription) &&
node.config.contains(SemanticsProperties.TextSelectionRange)
) {
return node.config[SemanticsProperties.TextSelectionRange].end
@@ -1510,7 +1510,7 @@
private fun isAccessibilitySelectionExtendable(node: SemanticsNode): Boolean {
// Currently only TextField is extendable. Static text may become extendable later.
- return !node.config.contains(SemanticsProperties.AccessibilityLabel) &&
+ return !node.config.contains(SemanticsProperties.ContentDescription) &&
node.config.contains(SemanticsProperties.Text)
}
@@ -1584,8 +1584,8 @@
}
// Note in android framework, TextView set this to its text. This is changed to
// prioritize content description, even for Text.
- if (node.config.contains(SemanticsProperties.AccessibilityLabel)) {
- return node.config[SemanticsProperties.AccessibilityLabel]
+ if (node.config.contains(SemanticsProperties.ContentDescription)) {
+ return node.config[SemanticsProperties.ContentDescription]
}
if (node.config.contains(SemanticsProperties.Text)) {
return node.config[SemanticsProperties.Text].text
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidOwner.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidOwner.kt
deleted file mode 100644
index 1e8e470..0000000
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidOwner.kt
+++ /dev/null
@@ -1,115 +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.compose.ui.platform
-
-import android.content.res.Configuration
-import android.graphics.Canvas
-import android.view.View
-import androidx.annotation.RestrictTo
-import androidx.compose.ui.node.LayoutNode
-import androidx.compose.ui.node.Owner
-import androidx.compose.ui.viewinterop.AndroidViewHolder
-import androidx.compose.ui.viewinterop.InternalInteropApi
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.ViewModelStoreOwner
-import androidx.savedstate.SavedStateRegistryOwner
-import org.jetbrains.annotations.TestOnly
-
-/**
- * Interface to be implemented by [Owner]s able to handle Android View specific functionality.
- */
-interface AndroidOwner : Owner {
-
- /**
- * The view backing this Owner.
- */
- val view: View
-
- /**
- * Called to inform the owner that a new Android [View] was [attached][Owner.onAttach]
- * to the hierarchy.
- */
- @OptIn(InternalInteropApi::class)
- fun addAndroidView(view: AndroidViewHolder, layoutNode: LayoutNode)
-
- /**
- * Called to inform the owner that an Android [View] was [detached][Owner.onDetach]
- * from the hierarchy.
- */
- @OptIn(InternalInteropApi::class)
- fun removeAndroidView(view: AndroidViewHolder)
-
- /**
- * Called to ask the owner to draw a child Android [View] to [canvas].
- */
- @OptIn(InternalInteropApi::class)
- fun drawAndroidView(view: AndroidViewHolder, canvas: Canvas)
-
- /**
- * Used for updating the ConfigurationAmbient when configuration changes - consume the
- * configuration ambient instead of changing this observer if you are writing a component
- * that adapts to configuration changes.
- */
- var configurationChangeObserver: (Configuration) -> Unit
-
- /**
- * Current [ViewTreeOwners]. Use [setOnViewTreeOwnersAvailable] if you want to
- * execute your code when the object will be created.
- */
- val viewTreeOwners: ViewTreeOwners?
-
- /**
- * The callback to be executed when [viewTreeOwners] is created and not-null anymore.
- * Note that this callback will be fired inline when it is already available
- */
- fun setOnViewTreeOwnersAvailable(callback: (ViewTreeOwners) -> Unit)
-
- /**
- * Called to invalidate the Android [View] sub-hierarchy handled by this Owner.
- */
- fun invalidateDescendants()
-
- /** @suppress */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- companion object {
- /**
- * Called after an [AndroidOwner] is created. Used by AndroidComposeTestRule to keep
- * track of all attached [AndroidComposeView]s. Not to be set or used by any other
- * component.
- */
- var onAndroidOwnerCreatedCallback: ((AndroidOwner) -> Unit)? = null
- @TestOnly
- set
- }
-
- /**
- * Combines objects populated via ViewTree*Owner
- */
- class ViewTreeOwners(
- /**
- * The [LifecycleOwner] associated with this owner.
- */
- val lifecycleOwner: LifecycleOwner,
- /**
- * The [ViewModelStoreOwner] associated with this owner.
- */
- val viewModelStoreOwner: ViewModelStoreOwner,
- /**
- * The [SavedStateRegistryOwner] associated with this owner.
- */
- val savedStateRegistryOwner: SavedStateRegistryOwner
- )
-}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewRootForTest.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewRootForTest.kt
new file mode 100644
index 0000000..b5c4e4e
--- /dev/null
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewRootForTest.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.platform
+
+import android.view.View
+import androidx.compose.ui.node.Owner
+import androidx.compose.ui.util.annotation.VisibleForTesting
+
+/**
+ * The marker interface to be implemented by the [View] backing the composition.
+ * To be used in tests.
+ */
+@VisibleForTesting
+// TODO(b/174747742) Introduce RootForTest and extend it instead of Owner
+interface ViewRootForTest : Owner {
+
+ /**
+ * The view backing this Owner.
+ */
+ val view: View
+
+ /**
+ * Returns true when the associated LifecycleOwner is in the resumed state
+ */
+ val isLifecycleInResumedState: Boolean
+
+ /**
+ * Whether the Owner has pending layout work.
+ */
+ val hasPendingMeasureOrLayout: Boolean
+
+ /**
+ * Called to invalidate the Android [View] sub-hierarchy handled by this [View].
+ */
+ fun invalidateDescendants()
+
+ companion object {
+ /**
+ * Called after an View implementing [ViewRootForTest] is created. Used by
+ * AndroidComposeTestRule to keep track of all attached ComposeViews. Not to be
+ * set or used by any other component.
+ */
+ @VisibleForTesting
+ var onViewCreatedCallback: ((ViewRootForTest) -> Unit)? = null
+ }
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/Wrapper.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/Wrapper.kt
index 8bd5f11..0e2b0e2 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/Wrapper.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/Wrapper.kt
@@ -27,6 +27,7 @@
import androidx.compose.runtime.CompositionReference
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.InternalComposeApi
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Providers
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.SlotTable
@@ -111,7 +112,7 @@
// instead.
@MainThread
@OptIn(ExperimentalComposeApi::class)
-internal actual fun actualSubcomposeInto(
+internal actual fun subcomposeInto(
container: LayoutNode,
parent: CompositionReference,
composable: @Composable () -> Unit
@@ -139,9 +140,9 @@
content: @Composable () -> Unit
): Composition {
GlobalSnapshotManager.ensureStarted()
- val composeView: AndroidOwner = window.decorView
+ val composeView: AndroidComposeView = window.decorView
.findViewById<ViewGroup>(android.R.id.content)
- .getChildAt(0) as? AndroidOwner
+ .getChildAt(0) as? AndroidComposeView
?: AndroidComposeView(this).also {
setContentView(it.view, DefaultLayoutParams)
}
@@ -168,7 +169,7 @@
GlobalSnapshotManager.ensureStarted()
val composeView =
if (childCount > 0) {
- getChildAt(0) as? AndroidOwner
+ getChildAt(0) as? AndroidComposeView
} else {
removeAllViews(); null
} ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) }
@@ -177,12 +178,12 @@
@OptIn(InternalComposeApi::class)
private fun doSetContent(
- owner: AndroidOwner,
+ owner: AndroidComposeView,
parent: CompositionReference,
content: @Composable () -> Unit
): Composition {
if (inspectionWanted(owner)) {
- owner.view.setTag(
+ owner.setTag(
R.id.inspection_slot_table_set,
Collections.newSetFromMap(WeakHashMap<SlotTable, Boolean>())
)
@@ -212,7 +213,7 @@
}
private class WrappedComposition(
- val owner: AndroidOwner,
+ val owner: AndroidComposeView,
val original: Composition
) : Composition, LifecycleEventObserver {
@@ -234,9 +235,17 @@
original.setContent {
@Suppress("UNCHECKED_CAST")
val inspectionTable =
- owner.view.getTag(R.id.inspection_slot_table_set) as?
+ owner.getTag(R.id.inspection_slot_table_set) as?
MutableSet<SlotTable>
- inspectionTable?.add(currentComposer.slotTable)
+ ?: (owner.parent as? View)?.getTag(R.id.inspection_slot_table_set)
+ as? MutableSet<SlotTable>
+ if (inspectionTable != null) {
+ inspectionTable.add(currentComposer.slotTable)
+ currentComposer.collectParameterInformation()
+ }
+
+ LaunchedEffect(owner) { owner.keyboardVisibilityEventLoop() }
+
Providers(InspectionTables provides inspectionTable) {
ProvideAndroidAmbients(owner, content)
}
@@ -283,6 +292,6 @@
*
* Instead check if the attributeSourceResourceMap is not empty.
*/
-private fun inspectionWanted(owner: AndroidOwner): Boolean =
+private fun inspectionWanted(owner: AndroidComposeView): Boolean =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
- owner.view.attributeSourceResourceMap.isNotEmpty()
+ owner.attributeSourceResourceMap.isNotEmpty()
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.kt
index feaece2..264c0fc 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.kt
@@ -29,6 +29,8 @@
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextRange
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.channels.Channel
import kotlin.math.roundToInt
/**
@@ -56,6 +58,12 @@
*/
private lateinit var imm: InputMethodManager
+ /**
+ * A channel that is used to send ShowKeyboard/HideKeyboard commands. Send 'true' for
+ * show Keyboard and 'false' to hide keyboard.
+ */
+ private val showKeyboardChannel = Channel<Boolean>(Channel.CONFLATED)
+
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
// focusedRect is null if there is not ongoing text input session. So safe to request
// latest focused rectangle whenever global layout has changed.
@@ -138,11 +146,26 @@
}
override fun showSoftwareKeyboard() {
- imm.showSoftInput(view, 0)
+ showKeyboardChannel.offer(true)
}
override fun hideSoftwareKeyboard() {
- imm.hideSoftInputFromWindow(view.windowToken, 0)
+ showKeyboardChannel.offer(false)
+ }
+
+ @OptIn(FlowPreview::class)
+ suspend fun keyboardVisibilityEventLoop() {
+ for (showKeyboard in showKeyboardChannel) {
+ // Even though we are using a conflated channel, and the producers and consumers are
+ // on the same thread, there is a possibility that we have a stale value in the channel
+ // because we start consuming from it before we finish producing all the values. We poll
+ // to make sure that we use the most recent value.
+ if (showKeyboardChannel.poll() ?: showKeyboard) {
+ imm.showSoftInput(view, 0)
+ } else {
+ imm.hideSoftInputFromWindow(view.windowToken, 0)
+ }
+ }
}
override fun onStateUpdated(oldValue: TextFieldValue?, newValue: TextFieldValue) {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.kt
index e12e11f..d9fcb3a 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.kt
@@ -31,6 +31,7 @@
import androidx.compose.runtime.Composition
import androidx.compose.runtime.CompositionReference
import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.compositionReference
import androidx.compose.runtime.onActive
import androidx.compose.runtime.onCommit
@@ -95,7 +96,7 @@
val dialog = remember(view, density) { DialogWrapper(view, density) }
dialog.onDismissRequest = onDismissRequest
- remember(properties) { dialog.setProperties(properties) }
+ SideEffect { dialog.setProperties(properties) }
onActive {
dialog.show()
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.kt
index 617ab73..01e2021 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.kt
@@ -30,6 +30,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composition
import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.compositionReference
import androidx.compose.runtime.emptyContent
import androidx.compose.runtime.onCommit
@@ -95,9 +96,11 @@
// Refresh anything that might have changed
popupLayout.onDismissRequest = onDismissRequest
popupLayout.testTag = AmbientPopupTestTag.current
- remember(popupPositionProvider) { popupLayout.setPositionProvider(popupPositionProvider) }
- remember(isFocusable) { popupLayout.setIsFocusable(isFocusable) }
- remember(properties) { popupLayout.setProperties(properties) }
+ SideEffect {
+ popupLayout.setPositionProvider(popupPositionProvider)
+ popupLayout.setIsFocusable(isFocusable)
+ popupLayout.setProperties(properties)
+ }
var composition: Composition? = null
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ExperimentalComposeUiApi.kt
similarity index 79%
copy from compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
copy to compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ExperimentalComposeUiApi.kt
index f9cb2fe..84259b4 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ExperimentalComposeUiApi.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.compose.ui.focus
+package androidx.compose.ui
-@RequiresOptIn("The Focus API is experimental and is likely to change in the future.")
-annotation class ExperimentalFocus
\ No newline at end of file
+@RequiresOptIn("This API is experimental and is likely to change in the future.")
+annotation class ExperimentalComposeUiApi
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusModifier.kt
index a8ac09c..a08c4e3 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusModifier.kt
@@ -17,7 +17,6 @@
package androidx.compose.ui
import androidx.compose.runtime.remember
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.focus.FocusState.Inactive
import androidx.compose.ui.node.ModifiedFocusNode
@@ -30,7 +29,6 @@
* A [Modifier.Element] that wraps makes the modifiers on the right into a Focusable. Use a
* different instance of [FocusModifier] for each focusable component.
*/
-@OptIn(ExperimentalFocus::class)
internal class FocusModifier(
initialFocus: FocusState,
// TODO(b/172265016): Make this a required parameter and remove the default value.
@@ -53,7 +51,6 @@
/**
* Add this modifier to a component to make it focusable.
*/
-@ExperimentalFocus
fun Modifier.focus(): Modifier = composed(inspectorInfo = debugInspectorInfo { name = "focus" }) {
remember { FocusModifier(Inactive) }
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusObserverModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusObserverModifier.kt
index 4546434..5188eaa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusObserverModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusObserverModifier.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.InspectorValueInfo
@@ -25,7 +24,6 @@
/**
* A [modifier][Modifier.Element] that can be used to observe focus state changes.
*/
-@ExperimentalFocus
interface FocusObserverModifier : Modifier.Element {
/**
* A callback that is called whenever focus state changes.
@@ -33,7 +31,6 @@
val onFocusChange: (FocusState) -> Unit
}
-@OptIn(ExperimentalFocus::class)
internal class FocusObserverModifierImpl(
override val onFocusChange: (FocusState) -> Unit,
inspectorInfo: InspectorInfo.() -> Unit
@@ -42,7 +39,6 @@
/**
* Add this modifier to a component to observe focus state changes.
*/
-@ExperimentalFocus
fun Modifier.focusObserver(onFocusChange: (FocusState) -> Unit): Modifier {
return this.then(
FocusObserverModifierImpl(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusRequesterModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusRequesterModifier.kt
index bd049b0..5d9a17d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusRequesterModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/FocusRequesterModifier.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.InspectorValueInfo
@@ -28,7 +27,6 @@
*
* @see FocusRequester
*/
-@ExperimentalFocus
interface FocusRequesterModifier : Modifier.Element {
/**
* An instance of [FocusRequester], that can be used to request focus state changes.
@@ -36,7 +34,6 @@
val focusRequester: FocusRequester
}
-@OptIn(ExperimentalFocus::class)
internal class FocusRequesterModifierImpl(
override val focusRequester: FocusRequester,
inspectorInfo: InspectorInfo.() -> Unit
@@ -45,7 +42,6 @@
/**
* Add this modifier to a component to observe changes to focus state.
*/
-@ExperimentalFocus
fun Modifier.focusRequester(focusRequester: FocusRequester): Modifier {
return this.then(
FocusRequesterModifierImpl(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/Autofill.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/Autofill.kt
index 09f55f3..2fca515 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/Autofill.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/Autofill.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.autofill
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.util.annotation.GuardedBy
@@ -26,6 +27,7 @@
* or cancel autofill as required. For instance, the [TextField] can call [requestAutofillForNode]
* when it gains focus, and [cancelAutofillForNode] when it loses focus.
*/
+@ExperimentalComposeUiApi
interface Autofill {
/**
@@ -66,6 +68,7 @@
*
* @property id A virtual id that is automatically generated for each node.
*/
+@ExperimentalComposeUiApi
data class AutofillNode(
val autofillTypes: List<AutofillType> = listOf(),
var boundingBox: Rect? = null,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillTree.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillTree.kt
index da01afb..5a8eb84 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillTree.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillTree.kt
@@ -16,6 +16,8 @@
package androidx.compose.ui.autofill
+import androidx.compose.ui.ExperimentalComposeUiApi
+
/**
* The autofill tree is a temporary data structure that is used before the Semantics Tree is
* implemented. This data structure is used by compose components to set autofill
@@ -27,6 +29,7 @@
* Since this is a temporary implementation, it is implemented as a list of [children], which is
* essentially a tree of height = 1
*/
+@ExperimentalComposeUiApi
class AutofillTree {
/**
* A map which contains [AutofillNode]s, where every node represents an autofillable field.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillType.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillType.kt
index 54af0f4..f0e1b16 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillType.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillType.kt
@@ -16,6 +16,8 @@
package androidx.compose.ui.autofill
+import androidx.compose.ui.ExperimentalComposeUiApi
+
/**
* Autofill type information.
*
@@ -24,6 +26,7 @@
* to use heuristics to determine the right value to use while
* autofilling the corresponding field.
*/
+@ExperimentalComposeUiApi
enum class AutofillType {
/**
* Indicates that the associated component can be aufofilled with an email address.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
index f396c48..8e93efa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
@@ -23,7 +23,6 @@
import androidx.compose.ui.gesture.PointerInputModifierImpl
import androidx.compose.ui.gesture.TapGestureFilter
-@ExperimentalFocus
interface FocusManager {
/**
* Call this function to clear focus from the currently focused component, and set the focus to
@@ -41,7 +40,6 @@
*
* @param focusModifier The modifier that will be used as the root focus modifier.
*/
-@ExperimentalFocus
internal class FocusManagerImpl(
private val focusModifier: FocusModifier = FocusModifier(Inactive)
) : FocusManager {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
index d4a8690..99c0968 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
@@ -31,7 +31,6 @@
*
* @see androidx.compose.ui.focusRequester
*/
-@ExperimentalFocus
class FocusRequester {
internal val focusRequesterNodes: MutableVector<ModifiedFocusRequesterNode> = mutableVectorOf()
@@ -98,4 +97,34 @@
}
return success
}
+
+ companion object {
+ /**
+ * Convenient way to create multiple [FocusRequester] instances.
+ */
+ object FocusRequesterFactory {
+ operator fun component1() = FocusRequester()
+ operator fun component2() = FocusRequester()
+ operator fun component3() = FocusRequester()
+ operator fun component4() = FocusRequester()
+ operator fun component5() = FocusRequester()
+ operator fun component6() = FocusRequester()
+ operator fun component7() = FocusRequester()
+ operator fun component8() = FocusRequester()
+ operator fun component9() = FocusRequester()
+ operator fun component10() = FocusRequester()
+ operator fun component11() = FocusRequester()
+ operator fun component12() = FocusRequester()
+ operator fun component13() = FocusRequester()
+ operator fun component14() = FocusRequester()
+ operator fun component15() = FocusRequester()
+ operator fun component16() = FocusRequester()
+ }
+
+ /**
+ * Convenient way to create multiple [FocusRequester]s, which can to be used to request
+ * focus, or to specify a focus traversal order.
+ */
+ fun createRefs() = FocusRequesterFactory
+ }
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusState.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusState.kt
index 0182bd3..68a9f7c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusState.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusState.kt
@@ -20,7 +20,6 @@
* Different states of the focus system. These are the states used by the Focus Nodes.
*
*/
-@ExperimentalFocus
enum class FocusState {
/**
* The focusable component is currently active (i.e. it receives key events).
@@ -56,7 +55,6 @@
*
* @return true if the component is focused, false otherwise.
*/
-@ExperimentalFocus
val FocusState.isFocused
get() = when (this) {
FocusState.Captured,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/DoubleTapGestureFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/DoubleTapGestureFilter.kt
index 39bb158..5d2b6f4 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/DoubleTapGestureFilter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/DoubleTapGestureFilter.kt
@@ -14,8 +14,6 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalPointerInput::class)
-
package androidx.compose.ui.gesture
import androidx.compose.runtime.remember
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/DragSlopExceededGestureFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/DragSlopExceededGestureFilter.kt
index 5c386f1..141b8ec 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/DragSlopExceededGestureFilter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/DragSlopExceededGestureFilter.kt
@@ -14,8 +14,6 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalPointerInput::class)
-
package androidx.compose.ui.gesture
import androidx.compose.runtime.remember
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/ExperimentalPointerInput.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/ExperimentalPointerInput.kt
deleted file mode 100644
index 0bfd44c..0000000
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/ExperimentalPointerInput.kt
+++ /dev/null
@@ -1,23 +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.compose.ui.gesture
-
-@RequiresOptIn(
- "This pointer input API is experimental and is likely to change before becoming " +
- "stable."
-)
-annotation class ExperimentalPointerInput
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/RawDragGestureFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/RawDragGestureFilter.kt
index 430326b..2fa2b6b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/RawDragGestureFilter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/RawDragGestureFilter.kt
@@ -14,8 +14,6 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalPointerInput::class)
-
package androidx.compose.ui.gesture
import androidx.compose.runtime.remember
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/TapGestureFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/TapGestureFilter.kt
index 57e5807..e3aba9e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/TapGestureFilter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/TapGestureFilter.kt
@@ -14,8 +14,6 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalPointerInput::class)
-
package androidx.compose.ui.gesture
import androidx.compose.runtime.remember
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/customevents/DelayUpEvent.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/customevents/DelayUpEvent.kt
index 10ea0f1..b2787cd 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/customevents/DelayUpEvent.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/customevents/DelayUpEvent.kt
@@ -17,7 +17,6 @@
package androidx.compose.ui.gesture.customevents
import androidx.compose.ui.gesture.DoubleTapGestureFilter
-import androidx.compose.ui.gesture.ExperimentalPointerInput
import androidx.compose.ui.gesture.TapGestureFilter
import androidx.compose.ui.input.pointer.CustomEvent
import androidx.compose.ui.input.pointer.PointerId
@@ -40,7 +39,6 @@
* @param pointers The pointers whose up events are being requested to be delayed.
*/
@Suppress("EqualsOrHashCode")
-@ExperimentalPointerInput
data class DelayUpEvent(var message: DelayUpMessage, val pointers: Set<PointerId>) : CustomEvent {
// Only generating hash code with immutable property.
@@ -52,7 +50,6 @@
/**
* The types of messages that can be dispatched.
*/
-@ExperimentalPointerInput
enum class DelayUpMessage {
/**
* Reports that future "up events" should not result in any normally related callbacks at
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/nestedscroll/NestedScrollDelegatingWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/nestedscroll/NestedScrollDelegatingWrapper.kt
new file mode 100644
index 0000000..6c29a8d
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/nestedscroll/NestedScrollDelegatingWrapper.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.gesture.nestedscroll
+
+import androidx.compose.runtime.collection.MutableVector
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.node.DelegatingLayoutNodeWrapper
+import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.node.LayoutNodeWrapper
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.minus
+import androidx.compose.ui.unit.plus
+
+internal class NestedScrollDelegatingWrapper(
+ wrapped: LayoutNodeWrapper,
+ nestedScrollModifier: NestedScrollModifier
+) : DelegatingLayoutNodeWrapper<NestedScrollModifier>(wrapped, nestedScrollModifier) {
+
+ // reference to the parent connection to properly dispatch or provide to children when detached
+ private var parentConnection: NestedScrollConnection? = null
+ set(value) {
+ modifier.dispatcher.parent = value
+ childScrollConnection.parent = value ?: NoOpConnection
+ field = value
+ }
+
+ // save last modifier until the next onModifierChanged() call to understand if we got new
+ // connection or a new dispatcher, therefore we need to update self and our children
+ private var lastModifier: NestedScrollModifier? = null
+
+ override fun onModifierChanged() {
+ super.onModifierChanged()
+ childScrollConnection.self = modifier.connection
+ modifier.dispatcher.parent = parentConnection
+ refreshSelfIfNeeded()
+ }
+
+ override var modifier: NestedScrollModifier
+ get() = super.modifier
+ set(value) {
+ lastModifier = super.modifier
+ super.modifier = value
+ }
+
+ override fun attach() {
+ super.attach()
+ refreshSelfIfNeeded()
+ }
+
+ override fun detach() {
+ super.detach()
+ refreshChildrenWithParentConnection(parentConnection)
+ lastModifier = null
+ }
+
+ override fun findPreviousNestedScrollWrapper() = this
+
+ override fun findNextNestedScrollWrapper() = this
+
+ private val childScrollConnection = ParentWrapperNestedScrollConnection(
+ parent = parentConnection ?: NoOpConnection,
+ self = nestedScrollModifier.connection
+ )
+
+ private fun refreshSelfIfNeeded() {
+ val localLastModifier = lastModifier
+ val modifierChanged = localLastModifier == null ||
+ localLastModifier.connection !== modifier.connection ||
+ localLastModifier.dispatcher !== modifier.dispatcher
+ if (modifierChanged && isAttached) {
+ parentConnection = super.findPreviousNestedScrollWrapper()?.childScrollConnection
+ refreshChildrenWithParentConnection(childScrollConnection)
+ lastModifier = modifier
+ }
+ }
+
+ /**
+ * Supply new parent connection for children. Initially children can do it themselves, but
+ * after runtime nestedscroll graph changes parents need to update their children.
+ *
+ * This is O(n) operation, so call only when parent really changes (connection changes,
+ * detach, attach, etc)
+ */
+ private fun refreshChildrenWithParentConnection(newParent: NestedScrollConnection?) {
+ nestedScrollChildrenResult.clear()
+ val nextNestedScrollWrapper = wrapped.findNextNestedScrollWrapper()
+ if (nextNestedScrollWrapper != null) {
+ nestedScrollChildrenResult.add(nextNestedScrollWrapper)
+ } else {
+ loopChildrenForNestedScroll(layoutNode._children)
+ }
+ nestedScrollChildrenResult.forEach {
+ it.parentConnection = newParent
+ }
+ }
+
+ private fun loopChildrenForNestedScroll(children: MutableVector<LayoutNode>) {
+ children.forEach { child ->
+ val nestedScrollChild =
+ child.outerLayoutNodeWrapper.findNextNestedScrollWrapper()
+ if (nestedScrollChild != null) {
+ nestedScrollChildrenResult.add(nestedScrollChild)
+ } else {
+ loopChildrenForNestedScroll(child._children)
+ }
+ }
+ }
+
+ // do not use directly, this is only for optimization.
+ // Populated and returned by findNestedScrollChildren.
+ private val nestedScrollChildrenResult = MutableVector<NestedScrollDelegatingWrapper>()
+}
+
+/**
+ * Parent-child binding contract. This wrapper guarantees pre-scroll/scroll/pre-fling/fling call
+ * order in the nested scroll chain.
+ */
+private class ParentWrapperNestedScrollConnection(
+ var parent: NestedScrollConnection,
+ var self: NestedScrollConnection
+) : NestedScrollConnection {
+
+ override fun onPreScroll(
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ val parentPreConsumed = parent.onPreScroll(available, source)
+ val selfPreConsumed = self.onPreScroll(available - parentPreConsumed, source)
+ return parentPreConsumed + selfPreConsumed
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ val selfConsumed = self.onPostScroll(consumed, available, source)
+ val parentConsumed =
+ parent.onPostScroll(consumed + selfConsumed, available - selfConsumed, source)
+ return selfConsumed + parentConsumed
+ }
+
+ override fun onPreFling(available: Velocity): Velocity {
+ val parentPreConsumed = parent.onPreFling(available)
+ val selfPreConsumed = self.onPreFling(available - parentPreConsumed)
+ return parentPreConsumed + selfPreConsumed
+ }
+
+ override fun onPostFling(
+ consumed: Velocity,
+ available: Velocity,
+ onFinished: (Velocity) -> Unit
+ ) {
+ val selfEnd = { selfConsumed: Velocity ->
+ val parentEnd = { parentConsumed: Velocity ->
+ onFinished.invoke(selfConsumed + parentConsumed)
+ }
+ parent.onPostFling(
+ consumed + selfConsumed,
+ available - selfConsumed,
+ parentEnd
+ )
+ }
+ self.onPostFling(consumed, available, selfEnd)
+ }
+}
+
+/**
+ * No-op parent that consumed nothing. Should be gone by b/174348612
+ */
+private val NoOpConnection: NestedScrollConnection = object : NestedScrollConnection {
+
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset =
+ Offset.Zero
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset =
+ Offset.Zero
+
+ override fun onPreFling(available: Velocity): Velocity = Velocity.Zero
+
+ override fun onPostFling(
+ consumed: Velocity,
+ available: Velocity,
+ onFinished: (Velocity) -> Unit
+ ) {
+ onFinished.invoke(Velocity.Zero)
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/nestedscroll/NestedScrollModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/nestedscroll/NestedScrollModifier.kt
new file mode 100644
index 0000000..fbbb177
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/nestedscroll/NestedScrollModifier.kt
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.gesture.nestedscroll
+
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Velocity
+
+/**
+ * A [Modifier.Element] that represents nested scroll node in the hierarchy
+ */
+internal interface NestedScrollModifier : Modifier.Element {
+
+ /**
+ * Nested scroll events dispatcher to notify nested scrolling system about scroll events.
+ * This is to be used by the nodes that are scrollable themselves to notify
+ * [NestedScrollConnection]s in the tree.
+ *
+ * Note: The [connection] passed to the [NestedScrollModifier] doesn't count as an ancestor
+ * since it's the node itself
+ */
+ val dispatcher: NestedScrollDispatcher
+
+ /**
+ * Nested scroll connection to participate in the nested scroll events chain. Implementing
+ * this connection allows to react on the nested scroll related events and influence
+ * scrolling descendants and ascendants
+ */
+ val connection: NestedScrollConnection
+}
+
+/**
+ * Interface to connect to the nested scroll system.
+ *
+ * Pass this connection to the [nestedScroll] modifier to participate in the nested scroll
+ * hierarchy and to receive nested scroll events when they are dispatched by the scrolling child
+ * (scrolling child - the element that actually receives scrolling events and dispatches them via
+ * [NestedScrollDispatcher]).
+ *
+ * @see NestedScrollDispatcher to learn how to dispatch nested scroll events to become a
+ * scrolling child
+ * @see nestedScroll to attach this connection to the nested scroll system
+ */
+interface NestedScrollConnection {
+
+ /**
+ * Pre scroll event chain. Called by children to allow parents to consume a portion of a drag
+ * event beforehand
+ *
+ * @param available the delta available to consume for pre scroll
+ * @param source the source of the scroll event
+ *
+ * @see NestedScrollSource
+ *
+ * @return the amount this connection consumed
+ */
+ fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = Offset.Zero
+
+ /**
+ * Post scroll event pass. This pass occurs when the dispatching (scrolling) descendant made
+ * their consumption and notifies ancestors with what's left for them to consume.
+ *
+ * @param consumed the amount that was consumed by all nested scroll nodes below the hierarchy
+ * @param available the amount of delta available for this connection to consume
+ * @param source source of the scroll
+ *
+ * @see NestedScrollSource
+ *
+ * @return the amount that was consumed by this connection
+ */
+ fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset = Offset.Zero
+
+ /**
+ * Pre fling event chain. Called by children when they are about to perform fling to
+ * allow parents to intercept and consume part of the initial velocity
+ *
+ * @param available the velocity which is available to pre consume and with which the child
+ * is about to fling
+ *
+ * @return the amount this connection wants to consume and take from the child
+ */
+ fun onPreFling(available: Velocity): Velocity = Velocity.Zero
+
+ /**
+ * Post fling event chain. Called by the child when it is finished flinging (and sending
+ * [onPreScroll] & [onPostScroll] events)
+ *
+ * @param consumed the amount of velocity consumed by the child
+ * @param available the amount of velocity left for a parent to fling after the child (if
+ * desired)
+ * @param onFinished callback to be called when this connection finished flinging, to
+ * be called with the amount of velocity consumed by the fling operation. This callback is
+ * crucial to be called in order to ensure nodes above will receive their [onPostFling].
+ */
+ // TODO: remove notifySelfFinish when b/174485541
+ fun onPostFling(
+ consumed: Velocity,
+ available: Velocity,
+ onFinished: (Velocity) -> Unit
+ ) {
+ onFinished(Velocity.Zero)
+ }
+}
+
+/**
+ * Nested scroll events dispatcher to notify the nested scroll system about the scrolling events
+ * that are happening on the element.
+ *
+ * If the element/modifier itself is able to receive scroll events (from the touch, fling,
+ * mouse, etc) and it would like to respect nested scrolling by notifying elements above, it should
+ * properly dispatch nested scroll events when being scrolled
+ *
+ * It is important to dispatch these events at the right time, provide valid information to the
+ * parents and react to the feedback received from them in order to provide good user experience
+ * with other nested scrolling nodes.
+ *
+ * @see nestedScroll for the reference of the nested scroll process and more details
+ * @see NestedScrollConnection to connect to the nested scroll system
+ */
+class NestedScrollDispatcher {
+
+ /**
+ * Parent to be set when attached to nested scrolling chain. `null` is valid and means there no
+ * nested scrolling parent above
+ */
+ internal var parent: NestedScrollConnection? = null
+
+ /**
+ * Dispatch pre scroll pass. This triggers [NestedScrollConnection.onPreScroll] on all the
+ * ancestors giving them possibility to pre-consume delta if they desire so.
+ *
+ * @param available the delta arrived from a scroll event
+ * @param source the source of the scroll event
+ *
+ * @return total delta that is pre-consumed by all ancestors in the chain. This delta is
+ * unavailable for this node to consume, so it should adjust the consumption accordingly
+ */
+ fun dispatchPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ return parent?.onPreScroll(available, source) ?: Offset.Zero
+ }
+
+ /**
+ * Dispatch nested post-scrolling pass. This triggers [NestedScrollConnection.onPostScroll] on
+ * all the ancestors giving them possibility to react of the scroll deltas that are left
+ * after the dispatching node itself and other [NestedScrollConnection]s below consumed the
+ * desired amount.
+ *
+ * @param consumed the amount that this node consumed already
+ * @param available the amount of delta left for ancestors
+ * @param source source of the scroll
+ *
+ * @return the amount of scroll that was consumed by all ancestors
+ */
+ fun dispatchPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ return parent?.onPostScroll(consumed, available, source) ?: Offset.Zero
+ }
+
+ /**
+ * Dispatch pre fling pass. This triggers [NestedScrollConnection.onPreFling] on all the
+ * ancestors giving them a possibility to react on the fling that is about to happen and
+ * consume part of the velocity.
+ *
+ * @param available velocity from the scroll evens that this node is about to fling with
+ *
+ * @return total velocity that is pre-consumed by all ancestors in the chain. This velocity is
+ * unavailable for this node to consume, so it should adjust the consumption accordingly
+ */
+ fun dispatchPreFling(available: Velocity): Velocity {
+ return parent?.onPreFling(available) ?: Velocity.Zero
+ }
+
+ /**
+ * Dispatch post fling pass. This triggers [NestedScrollConnection.onPostFling] on all the
+ * ancestors, giving them possibility to react of the velocity that is left after the
+ * dispatching node itself flung with the desired amount.
+ *
+ * @param consumed velocity already consumed by this node
+ * @param available velocity that is left for ancestors to consume
+ */
+ fun dispatchPostFling(consumed: Velocity, available: Velocity) {
+ parent?.onPostFling(consumed, available) {}
+ }
+}
+
+/**
+ * Possible sources of scroll events in the [NestedScrollConnection]
+ */
+enum class NestedScrollSource {
+ /**
+ * Dragging via mouse/touch/etc events
+ */
+ Drag,
+
+ /**
+ * Flinging after the drag has ended with velocity
+ */
+ Fling
+}
+
+/**
+ * Modify element to make it participate in the nested scrolling hierarchy.
+ *
+ * There are two ways to participate in the nested scroll: as a scrolling child by dispatching
+ * scrolling events via [NestedScrollDispatcher] to the nested scroll chain; and as a member of
+ * nested scroll chain by providing [NestedScrollConnection], which will be called when another
+ * nested scrolling child below dispatches scrolling events.
+ *
+ * It's a mandatory to participate as a [NestedScrollConnection] in the chain, but scrolling
+ * events dispatch is optional since there are cases when element wants to participate in the
+ * nested scroll, but not a scrollable thing itself.
+ *
+ * Note: It is recommended to reuse [NestedScrollConnection] and [NestedScrollDispatcher] objects
+ * between recompositions since different object will cause nested scroll graph to be
+ * recalculated unnecessary.
+ *
+ * There are 4 main passes in nested scrolling system:
+ *
+ * 1. Pre-scroll. This callback is triggered when the descendant is about to perform a scroll
+ * operation and gives parent an opportunity to consume part of child's delta beforehand. This
+ * pass should happen every time scrollable components receives delta and dispatches it via
+ * [NestedScrollDispatcher]. Dispatching child should take into account how much all ancestors
+ * above the hierarchy consumed and adjust the consumption accordingly.
+ *
+ * 2. Post-scroll. This callback is triggered when the descendant consumed the delta already
+ * (after taking into account what parents pre-consumed in 1.) and wants to notify the ancestors
+ * with the amount of delta unconsumed. This pass should happen every time scrollable components
+ * receives delta and dispatches it via [NestedScrollDispatcher]. Any parent that receives
+ * [NestedScrollConnection.onPostScroll] should consume no more than `left` and return the amount
+ * consumed.
+ *
+ * 3. Pre-fling. Pass that happens when the scrolling descendant stopped dragging and about to
+ * fling with the some velocity. This callback allows ancestors to consume part of the velocity.
+ * This pass should happen before the fling itself happens. Similar to pre-scroll, parent can
+ * consume part of the velocity and nodes below (including the dispatching child) should adjust
+ * their logic to accommodate only the velocity left.
+ *
+ * 4. Post-fling. Pass that happens after the scrolling descendant stopped flinging and wants to
+ * notify ancestors about that fact, providing velocity left to consume as a part of this. This
+ * pass should happen after the fling itself happens on the scrolling child. Ancestors of the
+ * dispatching node will have opportunity to fling themselves with the `velocityLeft` provided.
+ * Parent must call `notifySelfFinish` callback in order to continue the propagation of the
+ * velocity that is left to ancestors above.
+ *
+ * Example of the nested scrolling interaction where component both dispatches and consumed
+ * children's delta:
+ * @sample androidx.compose.ui.samples.NestedScrollSample
+ *
+ * @param connection connection to the nested scroll system to participate in the event chaining,
+ * receiving events when scrollable descendant is being scrolled.
+ * @param dispatcher object to be attached to the nested scroll system on which `dispatch*`
+ * methods can be called to notify ancestors within nested scroll system about scrolling happening
+ */
+fun Modifier.nestedScroll(
+ connection: NestedScrollConnection,
+ dispatcher: NestedScrollDispatcher? = null
+): Modifier = composed(
+ inspectorInfo = debugInspectorInfo {
+ name = "nestedScroll"
+ properties["connection"] = connection
+ properties["dispatcher"] = dispatcher
+ }
+) {
+ // provide noop dispatcher if needed
+ val resolvedDispatcher = dispatcher ?: remember { NestedScrollDispatcher() }
+ remember(connection, resolvedDispatcher) {
+ object : NestedScrollModifier {
+ override val dispatcher: NestedScrollDispatcher = resolvedDispatcher
+ override val connection: NestedScrollConnection = connection
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLocker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLocker.kt
index 7ea0160..8d43c717 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLocker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLocker.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.gesture.scrollorientationlocking
-import androidx.compose.ui.gesture.ExperimentalPointerInput
import androidx.compose.ui.input.pointer.CustomEvent
import androidx.compose.ui.input.pointer.CustomEventDispatcher
import androidx.compose.ui.input.pointer.PointerEventPass
@@ -46,7 +45,6 @@
* [onCancel] to use
* this correctly.
*/
-@ExperimentalPointerInput
class ScrollOrientationLocker(private val customEventDispatcher: CustomEventDispatcher) {
private var locker: InternalScrollOrientationLocker? = null
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorCompose.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorCompose.kt
index f65a27d..2078c8b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorCompose.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorCompose.kt
@@ -113,10 +113,14 @@
}
class VectorApplier(root: VNode) : AbstractApplier<VNode>(root) {
- override fun insert(index: Int, instance: VNode) {
+ override fun insertTopDown(index: Int, instance: VNode) {
current.asGroup().insertAt(index, instance)
}
+ override fun insertBottomUp(index: Int, instance: VNode) {
+ // Ignored as the tree is built top-down.
+ }
+
override fun remove(index: Int, count: Int) {
current.asGroup().remove(index, count)
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/ExperimentalKeyInput.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/ExperimentalKeyInput.kt
deleted file mode 100644
index f176c30..0000000
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/ExperimentalKeyInput.kt
+++ /dev/null
@@ -1,20 +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.compose.ui.input.key
-
-@RequiresOptIn("The Key Input API is experimental and is likely to change in the future.")
-annotation class ExperimentalKeyInput
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/Key.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/Key.kt
index fea9b39..69174ed6 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/Key.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/Key.kt
@@ -21,7 +21,6 @@
*
* @param keyCode an integer code representing the key pressed.
*/
-@ExperimentalKeyInput
expect inline class Key(val keyCode: Int) {
companion object {
/** Unknown key. */
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyEvent.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyEvent.kt
index 47f001c..f957cd8 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyEvent.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyEvent.kt
@@ -20,7 +20,6 @@
* When a user presses a key on a hardware keyboard, a [KeyEvent] is sent to the
* [KeyInputModifier] that is currently active.
*/
-@ExperimentalKeyInput
interface KeyEvent {
/**
* The key that was pressed.
@@ -85,7 +84,6 @@
/**
* The type of Key Event.
*/
-@ExperimentalKeyInput
enum class KeyEventType {
/**
* Unknown key event.
@@ -110,7 +108,6 @@
message = "Alt is replaced by KeyEvent.isAltPressed",
level = DeprecationLevel.WARNING
)
-@ExperimentalKeyInput
interface Alt {
/**
* Indicates whether the Alt key is pressed.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
index dc18697..db49588 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
@@ -29,7 +29,6 @@
* While implementing this callback, return true to stop propagation of this event. If you return
* false, the key event will be sent to this [keyInputFilter]'s parent.
*/
-@ExperimentalKeyInput
fun Modifier.keyInputFilter(onKeyEvent: (KeyEvent) -> Boolean): Modifier = composed(
inspectorInfo = debugInspectorInfo {
name = "keyInputFilter"
@@ -49,7 +48,6 @@
* to this [previewKeyInputFilter]'s child. If none of the children consume the event, it will be
* sent back up to the root [keyInputFilter] using the onKeyEvent callback.
*/
-@ExperimentalKeyInput
fun Modifier.previewKeyInputFilter(onPreviewKeyEvent: (KeyEvent) -> Boolean): Modifier = composed(
inspectorInfo = debugInspectorInfo {
name = "previewKeyInputFilter"
@@ -59,7 +57,6 @@
KeyInputModifier(onKeyEvent = null, onPreviewKeyEvent = onPreviewKeyEvent)
}
-@OptIn(ExperimentalKeyInput::class)
internal class KeyInputModifier(
val onKeyEvent: ((KeyEvent) -> Boolean)?,
val onPreviewKeyEvent: ((KeyEvent) -> Boolean)?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
index 2c19b0f..21d5ce8 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
@@ -21,7 +21,6 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
-import androidx.compose.ui.gesture.ExperimentalPointerInput
import androidx.compose.ui.platform.AmbientDensity
import androidx.compose.ui.platform.AmbientViewConfiguration
import androidx.compose.ui.platform.ViewConfiguration
@@ -43,11 +42,10 @@
* Receiver scope for awaiting pointer events in a call to [PointerInputScope.handlePointerInput].
*
* This is a restricted suspension scope. Code in this scope is always called undispatched and
- * may only suspend for calls to [awaitPointerEvent] or [awaitCustomEvent]. These functions
+ * may only suspend for calls to [awaitPointerEvent]. These functions
* resume synchronously and the caller may mutate the result **before** the next await call to
* affect the next stage of the input processing pipeline.
*/
-@ExperimentalPointerInput
@RestrictsSuspension
interface HandlePointerInputScope : Density {
/**
@@ -80,25 +78,11 @@
suspend fun awaitPointerEvent(
pass: PointerEventPass = PointerEventPass.Main
): PointerEvent
-
- /**
- * Suspend until a [CustomEvent] is reported to the specified input [pass].
- * [pass] defaults to [PointerEventPass.Main].
- *
- * [awaitCustomEvent] resumes **synchronously** in the restricted suspension scope. This
- * means that callers can react immediately to input after [awaitCustomEvent] returns
- * and affect both the current frame and the next handler or phase of the input processing
- * pipeline. Callers should mutate the returned [CustomEvent] before awaiting
- * another event to consume aspects of the event before the next stage of input processing runs. */
- suspend fun awaitCustomEvent(
- pass: PointerEventPass = PointerEventPass.Main
- ): CustomEvent
}
/**
* Receiver scope for [Modifier.pointerInput] that permits
- * [handling pointer input][handlePointerInput] and
- * [sending custom input events][customEventDispatcher].
+ * [handling pointer input][handlePointerInput].
*/
// Design note: this interface does _not_ implement CoroutineScope, even though doing so
// would more easily permit the use of launch {} inside Modifier.pointerInput {} blocks without
@@ -106,7 +90,6 @@
// gesture detectors as suspending extensions with a PointerInputScope receiver, also making this
// interface implement CoroutineScope would be an invitation to break structured concurrency in
// these extensions, leaving other launched coroutines running in the calling scope.
-@ExperimentalPointerInput
interface PointerInputScope : Density {
/**
* The measured size of the pointer input region. Input events will be reported with
@@ -116,13 +99,6 @@
val size: IntSize
/**
- * [customEventDispatcher] permits dispatching custom input events to the rest of the UI
- * in response to handling lower-level pointer input events. Accessing [customEventDispatcher]
- * before the first pointer input event is reported will throw [IllegalStateException].
- */
- val customEventDispatcher: CustomEventDispatcher
-
- /**
* The [ViewConfiguration] used to tune gesture detectors.
*/
val viewConfiguration: ViewConfiguration
@@ -149,7 +125,6 @@
* pointer input events. Extension functions on [PointerInputScope] or [HandlePointerInputScope]
* may be defined to perform higher-level gesture detection.
*/
-@ExperimentalPointerInput
fun Modifier.pointerInput(
block: suspend PointerInputScope.() -> Unit
): Modifier = composed(
@@ -184,7 +159,6 @@
*/
// TODO: Suppressing deprecation for synchronized; need to move to atomicfu wrapper
@Suppress("DEPRECATION_ERROR")
-@ExperimentalPointerInput
internal class SuspendingPointerInputFilter(
override val viewConfiguration: ViewConfiguration,
density: Density = Density(1f)
@@ -196,21 +170,9 @@
override val pointerInputFilter: PointerInputFilter
get() = this
- private var _customEventDispatcher: CustomEventDispatcher? = null
-
private var currentEvent: PointerEvent? = null
- /**
- * TODO: work out whether this is actually a race or not.
- * It shouldn't be, as we will have attached the [PointerInputModifier] during
- * composition-apply by the time the [LaunchedEffect] that would access this property
- * is dispatched and begins running.
- */
- override val customEventDispatcher: CustomEventDispatcher
- get() = _customEventDispatcher ?: error("customEventDispatcher not yet available")
-
override fun onInit(customEventDispatcher: CustomEventDispatcher) {
- _customEventDispatcher = customEventDispatcher
}
/**
@@ -328,9 +290,6 @@
}
override fun onCustomEvent(customEvent: CustomEvent, pass: PointerEventPass) {
- forEachCurrentPointerHandler(pass) {
- it.offerCustomEvent(customEvent, pass)
- }
}
override suspend fun <R> handlePointerInput(
@@ -373,7 +332,6 @@
private val completion: Continuation<R>,
) : HandlePointerInputScope, Density by this@SuspendingPointerInputFilter, Continuation<R> {
private var pointerAwaiter: CancellableContinuation<PointerEvent>? = null
- private var customAwaiter: CancellableContinuation<CustomEvent>? = null
private var awaitPass: PointerEventPass = PointerEventPass.Main
override val currentEvent: PointerEvent
@@ -394,21 +352,10 @@
}
}
- fun offerCustomEvent(event: CustomEvent, pass: PointerEventPass) {
- if (pass == awaitPass) {
- customAwaiter?.run {
- customAwaiter = null
- resume(event)
- }
- }
- }
-
// Called to run any finally blocks in the handlePointerInput block
fun cancel(cause: Throwable?) {
pointerAwaiter?.cancel(cause)
pointerAwaiter = null
- customAwaiter?.cancel(cause)
- customAwaiter = null
}
// context must be EmptyCoroutineContext for restricted suspension coroutines
@@ -428,12 +375,5 @@
awaitPass = pass
pointerAwaiter = continuation
}
-
- override suspend fun awaitCustomEvent(
- pass: PointerEventPass
- ): CustomEvent = suspendCancellableCoroutine { continuation ->
- awaitPass = pass
- customAwaiter = continuation
- }
}
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt
new file mode 100644
index 0000000..14f8ce2
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.layout
+
+import androidx.compose.ui.Modifier
+
+/**
+ * The public information about the layouts used internally as nodes in the Compose UI hierarchy.
+ */
+interface LayoutInfo {
+
+ /**
+ * This returns a new List of [Modifier]s and the coordinates and any extra information
+ * that may be useful. This is used for tooling to retrieve layout modifier and layer
+ * information.
+ */
+ fun getModifierInfo(): List<ModifierInfo>
+
+ /**
+ * The measured width of this layout and all of its modifiers.
+ */
+ val width: Int
+
+ /**
+ * The measured height of this layout and all of its modifiers.
+ */
+ val height: Int
+
+ /**
+ * Coordinates of just the contents of the layout, after being affected by all modifiers.
+ */
+ val coordinates: LayoutCoordinates
+
+ /**
+ * Whether or not this layout and all of its parents have been placed in the hierarchy.
+ */
+ val isPlaced: Boolean
+
+ /**
+ * Parent of this layout.
+ */
+ val parentInfo: LayoutInfo?
+
+ /**
+ * Returns true if this layout is currently a part of the layout tree.
+ */
+ val isAttached: Boolean
+}
+
+/**
+ * Used by tooling to examine the modifiers on a [LayoutInfo].
+ */
+class ModifierInfo(
+ val modifier: Modifier,
+ val coordinates: LayoutCoordinates,
+ val extra: Any? = null
+)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index 8acf50f..53805c9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -33,13 +33,11 @@
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.node.LayoutNode.LayoutState
import androidx.compose.ui.node.MeasureBlocks
-import androidx.compose.ui.node.isAttached
import androidx.compose.ui.platform.AmbientDensity
import androidx.compose.ui.platform.AmbientLayoutDirection
import androidx.compose.ui.platform.subcomposeInto
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.util.fastForEach
/**
* Analogue of [Layout] which allows to subcompose the actual content during the measuring stage
@@ -79,8 +77,6 @@
set(AmbientLayoutDirection.current, LayoutEmitHelper.setLayoutDirection)
}
)
-
- state.subcomposeIfRemeasureNotScheduled()
}
/**
@@ -159,15 +155,6 @@
return node.children
}
- fun subcomposeIfRemeasureNotScheduled() {
- val root = root!!
- if (root.layoutState != LayoutState.NeedsRemeasure && root.isAttached()) {
- root.foldedChildren.fastForEach {
- subcompose(it, nodeToNodeState.getValue(it))
- }
- }
- }
-
private fun subcompose(node: LayoutNode, nodeState: NodeState) {
node.ignoreModelReads {
val content = nodeState.content
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/TestModifierUpdater.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/TestModifierUpdater.kt
new file mode 100644
index 0000000..ce35d16
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/TestModifierUpdater.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.layout
+
+import androidx.compose.runtime.Applier
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.emit
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.LayoutEmitHelper
+import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.util.annotation.VisibleForTesting
+
+/** @hide */
+@Deprecated(
+ "It is a test API, do not use it in the real applications",
+ level = DeprecationLevel.ERROR
+)
+@VisibleForTesting
+class TestModifierUpdater internal constructor(private val node: LayoutNode) {
+ fun updateModifier(modifier: Modifier) {
+ node.modifier = modifier
+ }
+}
+
+/** @hide */
+@Deprecated(
+ "It is a test API, do not use it in the real applications",
+ level = DeprecationLevel.ERROR
+)
+@VisibleForTesting
+@Composable
+@Suppress("DEPRECATION_ERROR")
+fun TestModifierUpdaterLayout(onAttached: (TestModifierUpdater) -> Unit) {
+ val measureBlocks = MeasuringIntrinsicsMeasureBlocks { _, constraints ->
+ layout(constraints.maxWidth, constraints.maxHeight) {}
+ }
+ emit<LayoutNode, Applier<Any>>(
+ ctor = LayoutEmitHelper.constructor,
+ update = {
+ set(measureBlocks, LayoutEmitHelper.setMeasureBlocks)
+ set(Unit) { onAttached(TestModifierUpdater(this)) }
+ }
+ )
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
index 3289fab..111798f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
@@ -17,7 +17,6 @@
package androidx.compose.ui.node
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
@@ -48,6 +47,10 @@
*/
var isChained = false
+ // This is used by LayoutNode to mark LayoutNodeWrappers that are going to be reused
+ // because they match the modifier instance.
+ var toBeReusedForSameModifier = false
+
init {
wrapped.wrappedBy = this
}
@@ -132,11 +135,14 @@
return lastFocusWrapper
}
- @OptIn(ExperimentalFocus::class)
override fun propagateFocusStateChange(focusState: FocusState) {
wrappedBy?.propagateFocusStateChange(focusState)
}
+ override fun findPreviousNestedScrollWrapper() = wrappedBy?.findPreviousNestedScrollWrapper()
+
+ override fun findNextNestedScrollWrapper() = wrapped.findNextNestedScrollWrapper()
+
override fun findPreviousKeyInputWrapper() = wrappedBy?.findPreviousKeyInputWrapper()
override fun findNextKeyInputWrapper() = wrapped.findNextKeyInputWrapper()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DepthSortedSet.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DepthSortedSet.kt
index 62e25e8..d439d47 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DepthSortedSet.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DepthSortedSet.kt
@@ -58,7 +58,7 @@
}
fun add(node: LayoutNode) {
- check(node.isAttached())
+ check(node.isAttached)
if (extraAssertions) {
val usedDepth = mapOfOriginalDepth[node]
if (usedDepth == null) {
@@ -71,7 +71,7 @@
}
fun remove(node: LayoutNode) {
- check(node.isAttached())
+ check(node.isAttached)
val contains = set.remove(node)
if (extraAssertions) {
val usedDepth = mapOfOriginalDepth.remove(node)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
index f38542b..b75c27c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
@@ -16,9 +16,9 @@
package androidx.compose.ui.node
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollDelegatingWrapper
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.GraphicsLayerScope
@@ -59,13 +59,16 @@
override fun findLastFocusWrapper(): ModifiedFocusNode? = findPreviousFocusWrapper()
- @OptIn(ExperimentalFocus::class)
override fun propagateFocusStateChange(focusState: FocusState) {
wrappedBy?.propagateFocusStateChange(focusState)
}
override fun findPreviousKeyInputWrapper() = wrappedBy?.findPreviousKeyInputWrapper()
+ override fun findPreviousNestedScrollWrapper() = wrappedBy?.findPreviousNestedScrollWrapper()
+
+ override fun findNextNestedScrollWrapper(): NestedScrollDelegatingWrapper? = null
+
override fun findNextKeyInputWrapper(): ModifiedKeyInputNode? = null
override fun findLastKeyInputWrapper(): ModifiedKeyInputNode? = findPreviousKeyInputWrapper()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index 107116f..aefe6eb 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -17,13 +17,14 @@
import androidx.compose.runtime.collection.MutableVector
import androidx.compose.runtime.collection.mutableVectorOf
-import androidx.compose.ui.draw.DrawModifier
import androidx.compose.ui.FocusModifier
import androidx.compose.ui.FocusObserverModifier
import androidx.compose.ui.FocusRequesterModifier
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.ExperimentalFocus
+import androidx.compose.ui.draw.DrawModifier
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollDelegatingWrapper
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollModifier
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.input.key.KeyInputModifier
import androidx.compose.ui.input.pointer.PointerInputFilter
@@ -33,10 +34,12 @@
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.LayoutInfo
import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.ModifierInfo
import androidx.compose.ui.layout.OnGloballyPositionedModifier
import androidx.compose.ui.layout.OnRemeasuredModifier
import androidx.compose.ui.layout.ParentDataModifier
@@ -73,10 +76,9 @@
/**
* An element in the layout hierarchy, built with compose UI.
*/
-@OptIn(ExperimentalFocus::class)
-class LayoutNode : Measurable, Remeasurement, OwnerScope {
+class LayoutNode : Measurable, Remeasurement, OwnerScope, LayoutInfo {
- constructor() : this(false)
+ internal constructor() : this(false)
internal constructor(isVirtual: Boolean) {
this.isVirtual = isVirtual
@@ -139,13 +141,13 @@
/**
* The children of this LayoutNode, controlled by [insertAt], [move], and [removeAt].
*/
- val children: List<LayoutNode> get() = _children.asMutableList()
+ internal val children: List<LayoutNode> get() = _children.asMutableList()
/**
* The parent node in the LayoutNode hierarchy. This is `null` when the [LayoutNode]
* is not attached attached to a hierarchy or is the root of the hierarchy.
*/
- var parent: LayoutNode? = null
+ internal var parent: LayoutNode? = null
get() {
val parent = field
return if (parent != null && parent.isVirtual) parent.parent else parent
@@ -155,13 +157,19 @@
/**
* The view system [Owner]. This `null` until [attach] is called
*/
- var owner: Owner? = null
+ internal var owner: Owner? = null
private set
/**
+ * Returns true if this [LayoutNode] currently has an [LayoutNode.owner]. Semantically,
+ * this means that the LayoutNode is currently a part of a component tree.
+ */
+ override val isAttached: Boolean get() = owner != null
+
+ /**
* The tree depth of the [LayoutNode]. This is valid only when it is attached to a hierarchy.
*/
- var depth: Int = 0
+ internal var depth: Int = 0
/**
* The layout state the node is currently in.
@@ -290,7 +298,7 @@
* Set the [Owner] of this LayoutNode. This LayoutNode must not already be attached.
* [owner] must match its [parent].[owner].
*/
- fun attach(owner: Owner) {
+ internal fun attach(owner: Owner) {
check(this.owner == null) {
"Cannot attach $this as it already is attached"
}
@@ -318,7 +326,6 @@
parent?.requestRemeasure()
innerLayoutNodeWrapper.attach()
forEachDelegate { it.attach() }
- updateInnerLayerWrapper()
onAttach?.invoke(owner)
}
@@ -327,7 +334,7 @@
* and its [parent]'s [owner] must be `null` before calling this. This will also [detach] all
* children. After executing, the [owner] will be `null`.
*/
- fun detach() {
+ internal fun detach() {
val owner = owner
checkNotNull(owner) {
"Cannot detach node that is already detached!"
@@ -348,7 +355,6 @@
}
owner.onDetach(this)
this.owner = null
- _innerLayerWrapper = null
depth = 0
_foldedChildren.forEach { child ->
child.detach()
@@ -380,7 +386,7 @@
}
override val isValid: Boolean
- get() = isAttached()
+ get() = isAttached
override fun toString(): String {
return "${simpleIdentityToString(this, null)} children: ${children.size} " +
@@ -440,7 +446,7 @@
/**
* Blocks that define the measurement and intrinsic measurement of the layout.
*/
- var measureBlocks: MeasureBlocks = ErrorMeasureBlocks
+ internal var measureBlocks: MeasureBlocks = ErrorMeasureBlocks
set(value) {
if (field != value) {
field = value
@@ -451,7 +457,7 @@
/**
* The screen density to be used by this layout.
*/
- var density: Density = Density(1f)
+ internal var density: Density = Density(1f)
/**
* The scope used to run the [MeasureBlocks.measure]
@@ -466,7 +472,7 @@
/**
* The layout direction of the layout node.
*/
- var layoutDirection: LayoutDirection = LayoutDirection.Ltr
+ internal var layoutDirection: LayoutDirection = LayoutDirection.Ltr
set(value) {
if (field != value) {
field = value
@@ -477,12 +483,12 @@
/**
* The measured width of this layout and all of its [modifier]s. Shortcut for `size.width`.
*/
- val width: Int get() = outerMeasurablePlaceable.width
+ override val width: Int get() = outerMeasurablePlaceable.width
/**
* The measured height of this layout and all of its [modifier]s. Shortcut for `size.height`.
*/
- val height: Int get() = outerMeasurablePlaceable.height
+ override val height: Int get() = outerMeasurablePlaceable.height
/**
* The alignment lines of this layout, inherited + intrinsic
@@ -499,7 +505,7 @@
/**
* Whether or not this [LayoutNode] and all of its parents have been placed in the hierarchy.
*/
- var isPlaced: Boolean = false
+ override var isPlaced: Boolean = false
private set
/**
@@ -557,7 +563,7 @@
private val previousAlignmentLines = mutableMapOf<AlignmentLine, Int>()
@Deprecated("Temporary API to support ConstraintLayout prototyping.")
- var canMultiMeasure: Boolean = false
+ internal var canMultiMeasure: Boolean = false
internal val innerLayoutNodeWrapper: LayoutNodeWrapper = InnerPlaceable(this)
private val outerMeasurablePlaceable = OuterMeasurablePlaceable(this, innerLayoutNodeWrapper)
@@ -576,7 +582,20 @@
* The inner-most layer wrapper. Used for performance for LayoutNodeWrapper.findLayer().
*/
private var _innerLayerWrapper: LayoutNodeWrapper? = null
+ internal var innerLayerWrapperIsDirty = true
internal val innerLayerWrapper: LayoutNodeWrapper? get() {
+ if (innerLayerWrapperIsDirty) {
+ var delegate: LayoutNodeWrapper? = innerLayoutNodeWrapper
+ val final = outerLayoutNodeWrapper.wrappedBy
+ _innerLayerWrapper = null
+ while (delegate != final) {
+ if (delegate?.layer != null) {
+ _innerLayerWrapper = delegate
+ break
+ }
+ delegate = delegate?.wrappedBy
+ }
+ }
val layerWrapper = _innerLayerWrapper
if (layerWrapper != null) {
requireNotNull(layerWrapper.layer)
@@ -602,7 +621,7 @@
/**
* The [Modifier] currently applied to this node.
*/
- var modifier: Modifier = Modifier
+ internal var modifier: Modifier = Modifier
set(value) {
if (value == field) return
if (modifier != Modifier) {
@@ -613,10 +632,11 @@
val invalidateParentLayer = shouldInvalidateParentLayer()
copyWrappersToCache()
+ markReusedModifiers(value)
// Rebuild LayoutNodeWrapper
val oldOuterWrapper = outerMeasurablePlaceable.outerWrapper
- if (outerSemantics != null && isAttached()) {
+ if (outerSemantics != null && isAttached) {
owner!!.onSemanticsChange()
}
val addedCallback = hasNewPositioningCallback()
@@ -663,6 +683,9 @@
if (mod is PointerInputModifier) {
wrapper = PointerInputDelegatingWrapper(wrapper, mod).assignChained(toWrap)
}
+ if (mod is NestedScrollModifier) {
+ wrapper = NestedScrollDelegatingWrapper(wrapper, mod).assignChained(toWrap)
+ }
if (mod is LayoutModifier) {
wrapper = ModifiedLayoutNode(wrapper, mod).assignChained(toWrap)
}
@@ -679,13 +702,10 @@
outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper
outerMeasurablePlaceable.outerWrapper = outerWrapper
- if (isAttached()) {
+ if (isAttached) {
// call detach() on all removed LayoutNodeWrappers
wrapperCache.forEach {
it.detach()
- if (_innerLayerWrapper === it) {
- _innerLayerWrapper = null
- }
}
// attach() all new LayoutNodeWrappers
@@ -727,7 +747,7 @@
/**
* Coordinates of just the contents of the [LayoutNode], after being affected by all modifiers.
*/
- val coordinates: LayoutCoordinates
+ override val coordinates: LayoutCoordinates
get() = innerLayoutNodeWrapper
/**
@@ -757,7 +777,7 @@
*/
internal var needsOnPositionedDispatch = false
- fun place(x: Int, y: Int) {
+ internal fun place(x: Int, y: Int) {
Placeable.PlacementScope.executeWithRtlMirroringValues(
outerMeasurablePlaceable.measuredWidth,
layoutDirection
@@ -826,29 +846,11 @@
}
/**
- * Find the current inner layer.
- */
- private fun updateInnerLayerWrapper() {
- var delegate: LayoutNodeWrapper? = innerLayoutNodeWrapper
- val final = outerLayoutNodeWrapper.wrappedBy
- _innerLayerWrapper = null
- while (delegate != final) {
- if (delegate?.layer != null) {
- _innerLayerWrapper = delegate
- break
- }
- delegate = delegate?.wrappedBy
- }
- }
-
- /**
* Invoked when the parent placed the node. It will trigger the layout.
*/
internal fun onNodePlaced() {
val parent = parent
- updateInnerLayerWrapper()
-
var newZIndex = innerLayoutNodeWrapper.zIndex
forEachDelegate {
newZIndex += it.zIndex
@@ -1113,7 +1115,7 @@
* that may be useful. This is used for tooling to retrieve layout modifier and layer
* information.
*/
- fun getModifierInfo(): List<ModifierInfo> {
+ override fun getModifierInfo(): List<ModifierInfo> {
val infoList = mutableVectorOf<ModifierInfo>()
forEachDelegate { wrapper ->
wrapper as DelegatingLayoutNodeWrapper<*>
@@ -1146,8 +1148,16 @@
if (wrapperCache.isEmpty()) {
return null
}
- val index = wrapperCache.indexOfLast {
- it.modifier === modifier || it.modifier.nativeClass() == modifier.nativeClass()
+ // Look for exact match
+ var index = wrapperCache.indexOfLast {
+ it.toBeReusedForSameModifier && it.modifier === modifier
+ }
+
+ if (index < 0) {
+ // Look for class match
+ index = wrapperCache.indexOfLast {
+ !it.toBeReusedForSameModifier && it.modifier.nativeClass() == modifier.nativeClass()
+ }
}
if (index < 0) {
@@ -1182,6 +1192,17 @@
}
}
+ private fun markReusedModifiers(modifier: Modifier) {
+ wrapperCache.forEach {
+ it.toBeReusedForSameModifier = false
+ }
+
+ modifier.foldIn(Unit) { _, mod ->
+ val wrapper = wrapperCache.firstOrNull { it.modifier === mod }
+ wrapper?.toBeReusedForSameModifier = true
+ }
+ }
+
// Delegation from Measurable to measurableAndPlaceable
override fun measure(constraints: Constraints) =
outerMeasurablePlaceable.measure(constraints)
@@ -1237,17 +1258,14 @@
}
private fun shouldInvalidateParentLayer(): Boolean {
- if (innerLayerWrapper == null) {
- return true
- }
forEachDelegateIncludingInner {
- if (it is ModifiedDrawNode) {
- return true
- } else if (it.layer != null) {
+ if (it.layer != null) {
return false
+ } else if (it is ModifiedDrawNode) {
+ return true
}
}
- error("innerLayerWrapper should have been reached.")
+ return true
}
/**
@@ -1262,6 +1280,9 @@
}
}
+ override val parentInfo: LayoutInfo?
+ get() = parent
+
internal companion object {
private val ErrorMeasureBlocks: NoIntrinsicsMeasureBlocks =
object : NoIntrinsicsMeasureBlocks(
@@ -1329,22 +1350,6 @@
}
/**
- * Returns true if this [LayoutNode] currently has an [LayoutNode.owner]. Semantically,
- * this means that the LayoutNode is currently a part of a component tree.
- */
-@Suppress("NOTHING_TO_INLINE")
-internal inline fun LayoutNode.isAttached() = owner != null
-
-/**
- * Used by tooling to examine the modifiers on a [LayoutNode].
- */
-class ModifierInfo(
- val modifier: Modifier,
- val coordinates: LayoutCoordinates,
- val extra: Any? = null
-)
-
-/**
* Returns [LayoutNode.owner] or throws if it is null.
*/
internal fun LayoutNode.requireOwner(): Owner {
@@ -1356,8 +1361,9 @@
}
/**
- * Inserts a child [LayoutNode] at a last index. If this LayoutNode [isAttached]
- * then [child] will become [isAttached]ed also. [child] must have a `null` [LayoutNode.parent].
+ * Inserts a child [LayoutNode] at a last index. If this LayoutNode [LayoutNode.isAttached]
+ * then [child] will become [LayoutNode.isAttached] also. [child] must have a `null`
+ * [LayoutNode.parent].
*/
internal fun LayoutNode.add(child: LayoutNode) {
insertAt(children.size, child)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
index 6ad3889..a32ad2c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
@@ -18,12 +18,12 @@
package androidx.compose.ui.node
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.geometry.MutableRect
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.toRect
+import androidx.compose.ui.gesture.nestedscroll.NestedScrollDelegatingWrapper
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.graphics.Matrix
@@ -65,13 +65,14 @@
private var isClipping: Boolean = false
- private var layerBlock: (GraphicsLayerScope.() -> Unit)? = null
+ protected var layerBlock: (GraphicsLayerScope.() -> Unit)? = null
+ private set
private var _isAttached = false
override val isAttached: Boolean
get() {
if (_isAttached) {
- require(layoutNode.isAttached())
+ require(layoutNode.isAttached)
}
return _isAttached
}
@@ -109,9 +110,10 @@
var isShallowPlacing = false
private var _rectCache: MutableRect? = null
- private val rectCache: MutableRect get() = _rectCache ?: MutableRect(0f, 0f, 0f, 0f).also {
- _rectCache = it
- }
+ private val rectCache: MutableRect
+ get() = _rectCache ?: MutableRect(0f, 0f, 0f, 0f).also {
+ _rectCache = it
+ }
private val snapshotObserver get() = layoutNode.requireOwner().snapshotObserver
@@ -158,9 +160,7 @@
zIndex: Float,
layerBlock: (GraphicsLayerScope.() -> Unit)?
) {
- if (wrappedBy?.isShallowPlacing != true) {
- onLayerBlockUpdated(layerBlock)
- }
+ onLayerBlockUpdated(layerBlock)
if (this.position != position) {
this.position = position
val layer = layer
@@ -222,6 +222,7 @@
move(position)
}
updateLayerParameters()
+ layoutNode.innerLayerWrapperIsDirty = true
invalidateParentLayer()
} else if (blockHasBeenChanged) {
updateLayerParameters()
@@ -229,7 +230,7 @@
} else {
layer?.let {
it.destroy()
-
+ layoutNode.innerLayerWrapperIsDirty = true
invalidateParentLayer()
}
layer = null
@@ -499,14 +500,40 @@
}
/**
+ * Returns the first [NestedScrollDelegatingWrapper] in the wrapper list that wraps this
+ * [LayoutNodeWrapper].
+ *
+ * Note: This method tried to find [NestedScrollDelegatingWrapper] in the
+ * modifiers before the one wrapped with this [LayoutNodeWrapper] and goes up the hierarchy of
+ * [LayoutNode]s if needed.
+ */
+ abstract fun findPreviousNestedScrollWrapper(): NestedScrollDelegatingWrapper?
+
+ /**
+ * Returns the first [NestedScrollDelegatingWrapper] in the wrapper list that is wrapped by this
+ * [LayoutNodeWrapper].
+ *
+ * Note: This method only goes to the modifiers that follow the one wrapped by
+ * this [LayoutNodeWrapper], it doesn't to the children [LayoutNode]s.
+ */
+ abstract fun findNextNestedScrollWrapper(): NestedScrollDelegatingWrapper?
+
+ /**
* Returns the first [focus node][ModifiedFocusNode] in the wrapper list that wraps this
* [LayoutNodeWrapper].
+ *
+ * Note: This method tried to find [NestedScrollDelegatingWrapper] in the
+ * modifiers before the one wrapped with this [LayoutNodeWrapper] and goes up the hierarchy of
+ * [LayoutNode]s if needed.
*/
abstract fun findPreviousFocusWrapper(): ModifiedFocusNode?
/**
* Returns the next [focus node][ModifiedFocusNode] in the wrapper list that is wrapped by
* this [LayoutNodeWrapper].
+ *
+ * Note: This method only goes to the modifiers that follow the one wrapped by
+ * this [LayoutNodeWrapper], it doesn't to the children [LayoutNode]s.
*/
abstract fun findNextFocusWrapper(): ModifiedFocusNode?
@@ -521,7 +548,6 @@
* that wraps it. The focus state change must be propagated to the parents until we reach
* another [focus node][ModifiedFocusNode].
*/
- @OptIn(ExperimentalFocus::class)
abstract fun propagateFocusStateChange(focusState: FocusState)
/**
@@ -573,12 +599,19 @@
/**
* Returns the first [ModifiedKeyInputNode] in the wrapper list that wraps this
* [LayoutNodeWrapper].
+ *
+ * Note: This method tried to find [NestedScrollDelegatingWrapper] in the
+ * modifiers before the one wrapped with this [LayoutNodeWrapper] and goes up the hierarchy of
+ * [LayoutNode]s if needed.
*/
abstract fun findPreviousKeyInputWrapper(): ModifiedKeyInputNode?
/**
* Returns the next [ModifiedKeyInputNode] in the wrapper list that is wrapped by this
* [LayoutNodeWrapper].
+ *
+ * Note: This method only goes to the modifiers that follow the one wrapped by
+ * this [LayoutNodeWrapper], it doesn't to the children [LayoutNode]s.
*/
abstract fun findNextKeyInputWrapper(): ModifiedKeyInputNode?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
index 126c75e..8024fb6 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
@@ -189,7 +189,7 @@
* Iterates through all LayoutNodes that have requested layout and measures and lays them out
*/
fun measureAndLayout(): Boolean {
- require(root.isAttached())
+ require(root.isAttached)
require(root.isPlaced)
require(!duringMeasureLayout)
// we don't need to measure any children unless we have the correct root constraints
@@ -225,7 +225,7 @@
// execute postponed `onRequestMeasure`
if (postponedMeasureRequests.isNotEmpty()) {
postponedMeasureRequests.fastForEach {
- if (it.isAttached()) {
+ if (it.isAttached) {
requestRemeasure(it)
}
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt
index 7da03ef..cccc303 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt
@@ -17,7 +17,6 @@
package androidx.compose.ui.node
import androidx.compose.ui.FocusModifier
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.focus.FocusState.Active
import androidx.compose.ui.focus.FocusState.ActiveParent
@@ -28,9 +27,6 @@
import androidx.compose.ui.focus.searchChildrenForFocusNode
import androidx.compose.ui.util.fastForEach
-@OptIn(
- ExperimentalFocus::class,
-)
internal class ModifiedFocusNode(
wrapped: LayoutNodeWrapper,
modifier: FocusModifier
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusObserverNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusObserverNode.kt
index 2aa55e8..92cf057 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusObserverNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusObserverNode.kt
@@ -17,12 +17,10 @@
package androidx.compose.ui.node
import androidx.compose.ui.FocusObserverModifier
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.focus.FocusState.Inactive
import androidx.compose.ui.focus.searchChildrenForFocusNode
-@OptIn(ExperimentalFocus::class)
internal class ModifiedFocusObserverNode(
wrapped: LayoutNodeWrapper,
modifier: FocusObserverModifier
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusRequesterNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusRequesterNode.kt
index 03f8140..569edcc 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusRequesterNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusRequesterNode.kt
@@ -17,13 +17,9 @@
package androidx.compose.ui.node
import androidx.compose.ui.FocusRequesterModifier
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.searchChildrenForFocusNode
-@OptIn(
- ExperimentalFocus::class
-)
internal class ModifiedFocusRequesterNode(
wrapped: LayoutNodeWrapper,
modifier: FocusRequesterModifier
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedKeyInputNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedKeyInputNode.kt
index 1c4856a..17b728f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedKeyInputNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedKeyInputNode.kt
@@ -16,11 +16,9 @@
package androidx.compose.ui.node
-import androidx.compose.ui.input.key.ExperimentalKeyInput
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyInputModifier
-@OptIn(ExperimentalKeyInput::class)
internal class ModifiedKeyInputNode(wrapped: LayoutNodeWrapper, modifier: KeyInputModifier) :
DelegatingLayoutNodeWrapper<KeyInputModifier>(wrapped, modifier) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedLayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedLayoutNode.kt
index d01fe4c..8e952e0 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedLayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedLayoutNode.kt
@@ -66,7 +66,7 @@
}
// Place our wrapped to obtain their position inside ourselves.
isShallowPlacing = true
- placeAt(this.position, zIndex = 0f, null)
+ placeAt(this.position, this.zIndex, this.layerBlock)
isShallowPlacing = false
return if (line is HorizontalAlignmentLine) {
positionInWrapped + wrapped.position.y
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
index b049c55..97e3311 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
@@ -15,13 +15,12 @@
*/
package androidx.compose.ui.node
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.autofill.Autofill
import androidx.compose.ui.autofill.AutofillTree
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.hapticfeedback.HapticFeedback
-import androidx.compose.ui.input.key.ExperimentalKeyInput
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.TextToolbar
@@ -67,11 +66,13 @@
* TODO(ralu): Replace with SemanticsTree. This is a temporary hack until we have a semantics
* tree implemented.
*/
+ @ExperimentalComposeUiApi
val autofillTree: AutofillTree
/**
* The [Autofill] class can be used to perform autofill operations. It is used as an ambient.
*/
+ @ExperimentalComposeUiApi
val autofill: Autofill?
val density: Density
@@ -83,7 +84,6 @@
/**
* Provide a focus manager that controls focus within Compose.
*/
- @ExperimentalFocus
val focusManager: FocusManager
/**
@@ -114,11 +114,6 @@
fun onRequestRelayout(layoutNode: LayoutNode)
/**
- * Whether the Owner has pending layout work.
- */
- val hasPendingMeasureOrLayout: Boolean
-
- /**
* Called by [LayoutNode] when it is attached to the view system and now has an owner.
* This is used by [Owner] to track which nodes are associated with it. It will only be
* called when [node] is not already attached to an owner.
@@ -150,7 +145,6 @@
*
* @return true if the event was consumed. False otherwise.
*/
- @ExperimentalKeyInput
fun sendKeyEvent(keyEvent: KeyEvent): Boolean
/**
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt
index bfbb003..c4f6b6d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt
@@ -20,9 +20,9 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Providers
import androidx.compose.runtime.staticAmbientOf
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.autofill.Autofill
import androidx.compose.ui.autofill.AutofillTree
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.node.Owner
@@ -52,19 +52,7 @@
/**
* The ambient that can be used to trigger autofill actions. Eg. [Autofill.requestAutofillForNode].
*/
-@Suppress("AmbientNaming")
-@Deprecated(
- "Renamed to AmbientAutofill",
- replaceWith = ReplaceWith(
- "AmbientAutofill",
- "androidx.compose.ui.platform.AmbientAutofill"
- )
-)
-val AutofillAmbient get() = AmbientAutofill
-
-/**
- * The ambient that can be used to trigger autofill actions. Eg. [Autofill.requestAutofillForNode].
- */
+@ExperimentalComposeUiApi
val AmbientAutofill = staticAmbientOf<Autofill?>()
/**
@@ -73,22 +61,7 @@
* [AutofillTree] is a temporary data structure that will be replaced by Autofill Semantics
* (b/138604305).
*/
-@Suppress("AmbientNaming")
-@Deprecated(
- "Renamed to AmbientAutofillTree",
- replaceWith = ReplaceWith(
- "AmbientAutofillTree",
- "androidx.compose.ui.platform.AmbientAutofillTree"
- )
-)
-val AutofillTreeAmbient get() = AmbientAutofillTree
-
-/**
- * The ambient that can be used to add
- * [AutofillNode][import androidx.compose.ui.autofill.AutofillNode]s to the autofill tree. The
- * [AutofillTree] is a temporary data structure that will be replaced by Autofill Semantics
- * (b/138604305).
- */
+@ExperimentalComposeUiApi
val AmbientAutofillTree = staticAmbientOf<AutofillTree>()
/**
@@ -146,13 +119,11 @@
"androidx.compose.ui.platform.AmbientFocusManager"
)
)
-@ExperimentalFocus
val FocusManagerAmbient get() = AmbientFocusManager
/**
* The ambient that can be used to control focus within Compose.
*/
-@ExperimentalFocus
val AmbientFocusManager = staticAmbientOf<FocusManager>()
/**
@@ -292,7 +263,7 @@
*/
val AmbientWindowManager = staticAmbientOf<WindowManager>()
-@OptIn(ExperimentalFocus::class)
+@ExperimentalComposeUiApi
@Composable
internal fun ProvideCommonAmbients(
owner: Owner,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Subcomposition.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Subcomposition.kt
index c741795..7e4506e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Subcomposition.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Subcomposition.kt
@@ -18,20 +18,12 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composition
import androidx.compose.runtime.CompositionReference
-import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.util.annotation.MainThread
-internal expect fun actualSubcomposeInto(
- container: LayoutNode,
- parent: CompositionReference,
- composable: @Composable () -> Unit
-): Composition
-
-@OptIn(ExperimentalComposeApi::class)
@MainThread
-fun subcomposeInto(
+internal expect fun subcomposeInto(
container: LayoutNode,
parent: CompositionReference,
composable: @Composable () -> Unit
-): Composition = actualSubcomposeInto(container, parent, composable)
\ No newline at end of file
+): Composition
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selectable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selectable.kt
index 26d168e..b98c168 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selectable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selectable.kt
@@ -20,11 +20,13 @@
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
/**
* Provides [Selection] information for a composable to SelectionContainer. Composables who can
* be selected should subscribe to [SelectionRegistrar] using this interface.
*/
+@ExperimentalTextApi
interface Selectable {
/**
* Returns [Selection] information for a selectable composable. If no selection can be provided
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selection.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selection.kt
index 714ac17..bb80ac9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selection.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selection.kt
@@ -17,6 +17,7 @@
package androidx.compose.ui.selection
import androidx.compose.runtime.Immutable
+import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.style.ResolvedTextDirection
@@ -24,6 +25,7 @@
* Information about the current Selection.
*/
@Immutable
+@OptIn(ExperimentalTextApi::class)
data class Selection(
/**
* Information about the start of the selection.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionContainer.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionContainer.kt
index 3e4b606..be22667 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionContainer.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionContainer.kt
@@ -135,17 +135,12 @@
handle = null
)
}
- SelectionFloatingToolBar(manager = manager)
}
}
}
onDispose {
manager.selection = null
+ manager.hideSelectionToolbar()
}
}
-
-@Composable
-private fun SelectionFloatingToolBar(manager: SelectionManager) {
- manager.showSelectionToolbar()
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionManager.kt
index b6cbf8e..ce33357 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionManager.kt
@@ -31,6 +31,7 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.TextToolbarStatus
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.InternalTextApi
import androidx.compose.ui.text.length
import androidx.compose.ui.text.subSequence
@@ -40,7 +41,10 @@
/**
* A bridge class between user interaction to the text composables for text selection.
*/
-@OptIn(InternalTextApi::class)
+@OptIn(
+ InternalTextApi::class,
+ ExperimentalTextApi::class
+)
internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
/**
* The current selection.
@@ -49,7 +53,6 @@
set(value) {
field = value
updateHandleOffsets()
- hideSelectionToolbar()
}
/**
@@ -123,9 +126,20 @@
init {
selectionRegistrar.onPositionChangeCallback = {
updateHandleOffsets()
+ updateSelectionToolbarPosition()
+ }
+
+ selectionRegistrar.onSelectionUpdateStartCallback = { layoutCoordinates, startPosition ->
+ updateSelection(
+ startPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
+ endPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
+ isStartHandle = true,
+ longPress = true
+ )
hideSelectionToolbar()
}
- selectionRegistrar.onUpdateSelectionCallback =
+
+ selectionRegistrar.onSelectionUpdateCallback =
{ layoutCoordinates, startPosition, endPosition ->
updateSelection(
startPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
@@ -134,6 +148,10 @@
longPress = true
)
}
+
+ selectionRegistrar.onSelectionUpdateEndCallback = {
+ showSelectionToolbar()
+ }
}
private fun updateHandleOffsets() {
@@ -265,12 +283,9 @@
}
}
- private fun hideSelectionToolbar() {
+ internal fun hideSelectionToolbar() {
if (textToolbar?.status == TextToolbarStatus.Shown) {
- val selection = selection
- if (selection == null) {
- textToolbar?.hide()
- }
+ textToolbar?.hide()
}
}
@@ -356,12 +371,14 @@
endPosition = Offset(-1f, -1f),
previousSelection = selection
)
+ hideSelectionToolbar()
if (selection != null) onSelectionChange(null)
}
fun handleDragObserver(isStartHandle: Boolean): DragObserver {
return object : DragObserver {
override fun onStart(downPosition: Offset) {
+ hideSelectionToolbar()
val selection = selection!!
// The LayoutCoordinates of the composable where the drag gesture should begin. This
// is used to convert the position of the beginning of the drag gesture from the
@@ -375,13 +392,15 @@
// The position of the character where the drag gesture should begin. This is in
// the composable coordinates.
val beginCoordinates = getAdjustedCoordinates(
- if (isStartHandle)
+ if (isStartHandle) {
selection.start.selectable.getHandlePosition(
selection = selection, isStartHandle = true
- ) else
+ )
+ } else {
selection.end.selectable.getHandlePosition(
selection = selection, isStartHandle = false
)
+ }
)
// Convert the position where drag gesture begins from composable coordinates to
@@ -434,6 +453,14 @@
)
return dragDistance
}
+
+ override fun onStop(velocity: Offset) {
+ showSelectionToolbar()
+ }
+
+ override fun onCancel() {
+ showSelectionToolbar()
+ }
}
}
@@ -468,6 +495,7 @@
return lhs?.merge(rhs) ?: rhs
}
+@OptIn(ExperimentalTextApi::class)
internal fun getCurrentSelectedText(
selectable: Selectable,
selection: Selection
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrar.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrar.kt
index 3dbff76..7e1cded 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrar.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrar.kt
@@ -19,10 +19,12 @@
import androidx.compose.runtime.ambientOf
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.ExperimentalTextApi
/**
* An interface allowing a composable to subscribe and unsubscribe to selection changes.
*/
+@ExperimentalTextApi
interface SelectionRegistrar {
/**
* Subscribe to SelectionContainer selection changes.
@@ -38,20 +40,58 @@
* When the Global Position of a subscribed [Selectable] changes, this method
* is called.
*/
- fun onPositionChange()
+ fun notifyPositionChange()
/**
- * When selection changes, this method is called.
+ * Call this method to notify the [SelectionContainer] that the selection has been initiated.
+ * Depends on the input, [notifySelectionUpdate] may be called repeatedly after
+ * [notifySelectionUpdateStart] is called. And [notifySelectionUpdateEnd] should always be
+ * called after selection finished.
+ * For example:
+ * 1. User long pressed the text and then release. [notifySelectionUpdateStart] should be
+ * called followed by [notifySelectionUpdateEnd] being called once.
+ * 2. User long pressed the text and then drag a distance and then release.
+ * [notifySelectionUpdateStart] should be called first after the user long press, and then
+ * [notifySelectionUpdate] is called several times reporting the updates, in the end
+ * [notifySelectionUpdateEnd] is called to finish the selection.
+ *
+ * @param layoutCoordinates [LayoutCoordinates] of the [Selectable].
+ * @param startPosition coordinates of where the selection is initiated.
+ *
+ * @see notifySelectionUpdate
+ * @see notifySelectionUpdateEnd
+ */
+ fun notifySelectionUpdateStart(
+ layoutCoordinates: LayoutCoordinates,
+ startPosition: Offset
+ )
+
+ /**
+ * Call this method to notify the [SelectionContainer] that the selection has been updated.
+ * The caller of this method should make sure that [notifySelectionUpdateStart] is always
+ * called once before calling this function. And [notifySelectionUpdateEnd] is always called
+ * once after the all updates finished.
*
* @param layoutCoordinates [LayoutCoordinates] of the [Selectable].
* @param startPosition coordinates of where the selection starts.
* @param endPosition coordinates of where the selection ends.
+ *
+ * @see notifySelectionUpdateStart
+ * @see notifySelectionUpdateEnd
*/
- fun onUpdateSelection(
+ fun notifySelectionUpdate(
layoutCoordinates: LayoutCoordinates,
startPosition: Offset,
- endPosition: Offset
+ endPosition: Offset,
)
+
+ /**
+ * Call this method to notify the [SelectionContainer] that the selection update has stopped.
+ *
+ * @see notifySelectionUpdateStart
+ * @see notifySelectionUpdate
+ */
+ fun notifySelectionUpdateEnd()
}
/**
@@ -72,4 +112,5 @@
* Ambient of SelectionRegistrar. Composables that implement selection logic can use this ambient
* to get a [SelectionRegistrar] in order to subscribe and unsubscribe to [SelectionRegistrar].
*/
+@OptIn(ExperimentalTextApi::class)
val AmbientSelectionRegistrar = ambientOf<SelectionRegistrar?>()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrarImpl.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrarImpl.kt
index c42a8d1..dd9250d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrarImpl.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrarImpl.kt
@@ -18,7 +18,9 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.ExperimentalTextApi
+@OptIn(ExperimentalTextApi::class)
internal class SelectionRegistrarImpl : SelectionRegistrar {
/**
* A flag to check if the [Selectable]s have already been sorted.
@@ -42,6 +44,21 @@
*/
internal var onPositionChangeCallback: (() -> Unit)? = null
+ /**
+ * The callback to be invoked when the selection is initiated.
+ */
+ internal var onSelectionUpdateStartCallback: ((LayoutCoordinates, Offset) -> Unit)? = null
+
+ /**
+ * The callback to be invoked when the selection is updated.
+ */
+ internal var onSelectionUpdateCallback: ((LayoutCoordinates, Offset, Offset) -> Unit)? = null
+
+ /**
+ * The callback to be invoked when selection update finished.
+ */
+ internal var onSelectionUpdateEndCallback: (() -> Unit)? = null
+
override fun subscribe(selectable: Selectable): Selectable {
_selectables.add(selectable)
sorted = false
@@ -87,27 +104,29 @@
return selectables
}
- override fun onPositionChange() {
+ override fun notifyPositionChange() {
// Set the variable sorted to be false, when the global position of a registered
// selectable changes.
sorted = false
onPositionChangeCallback?.invoke()
}
- /**
- * The callback to be invoked when the selection change was triggered.
- */
- internal var onUpdateSelectionCallback: ((LayoutCoordinates, Offset, Offset) -> Unit)? = null
+ override fun notifySelectionUpdateStart(
+ layoutCoordinates: LayoutCoordinates,
+ startPosition: Offset
+ ) {
+ onSelectionUpdateStartCallback?.invoke(layoutCoordinates, startPosition)
+ }
- override fun onUpdateSelection(
+ override fun notifySelectionUpdate(
layoutCoordinates: LayoutCoordinates,
startPosition: Offset,
endPosition: Offset
) {
- onUpdateSelectionCallback?.invoke(
- layoutCoordinates,
- startPosition,
- endPosition
- )
+ onSelectionUpdateCallback?.invoke(layoutCoordinates, startPosition, endPosition)
+ }
+
+ override fun notifySelectionUpdateEnd() {
+ onSelectionUpdateEndCallback?.invoke()
}
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
index 8b28026..bca6c84 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
@@ -23,8 +23,10 @@
import androidx.compose.ui.layout.globalBounds
import androidx.compose.ui.layout.globalPosition
import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.layout.LayoutInfo
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.node.LayoutNodeWrapper
+import androidx.compose.ui.node.Owner
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastForEach
@@ -66,9 +68,20 @@
val id: Int = layoutNodeWrapper.modifier.id
/**
+ * The [LayoutInfo] that this is associated with.
+ */
+ val layoutInfo: LayoutInfo = layoutNodeWrapper.layoutNode
+
+ /**
+ * The [Owner] this node is attached to.
+ */
+ // TODO(b/174747742) Stop using Owner in tests and use RootForTest instead
+ val owner: Owner? get() = layoutNode.owner
+
+ /**
* The [LayoutNode] that this is associated with.
*/
- val layoutNode: LayoutNode = layoutNodeWrapper.layoutNode
+ internal val layoutNode: LayoutNode = layoutNodeWrapper.layoutNode
// GEOMETRY
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
index e6d3278..ca1996d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
@@ -33,10 +33,10 @@
* Developer-set content description of the semantics node. If this is not set, accessibility
* services will present the [Text] of this node as content part.
*
- * @see SemanticsPropertyReceiver.accessibilityLabel
+ * @see SemanticsPropertyReceiver.contentDescription
*/
- val AccessibilityLabel = SemanticsPropertyKey<String>(
- name = "AccessibilityLabel",
+ val ContentDescription = SemanticsPropertyKey<String>(
+ name = "ContentDescription",
mergePolicy = { parentValue, childValue ->
if (parentValue == null) {
childValue
@@ -52,14 +52,14 @@
* [AccessibilityRangeInfo], but it is not guaranteed and the format will be decided by
* accessibility services.
*
- * @see SemanticsPropertyReceiver.accessibilityValue
+ * @see SemanticsPropertyReceiver.stateDescription
*/
- val AccessibilityValue = SemanticsPropertyKey<String>("AccessibilityValue")
+ val StateDescription = SemanticsPropertyKey<String>("StateDescription")
/**
* The node is a range with current value.
*
- * @see SemanticsPropertyReceiver.accessibilityValueRange
+ * @see SemanticsPropertyReceiver.stateDescriptionRange
*/
val AccessibilityRangeInfo =
SemanticsPropertyKey<AccessibilityRangeInfo>("AccessibilityRangeInfo")
@@ -410,9 +410,15 @@
* Developer-set content description of the semantics node. If this is not set, accessibility
* services will present the text of this node as content part.
*
- * @see SemanticsProperties.AccessibilityLabel
+ * @see SemanticsProperties.ContentDescription
*/
-var SemanticsPropertyReceiver.accessibilityLabel by SemanticsProperties.AccessibilityLabel
+var SemanticsPropertyReceiver.contentDescription by SemanticsProperties.ContentDescription
+
+@Deprecated(
+ "accessibilityLabel was renamed to contentDescription",
+ ReplaceWith("contentDescription", "androidx.compose.ui.semantics")
+)
+var SemanticsPropertyReceiver.accessibilityLabel by SemanticsProperties.ContentDescription
/**
* Developer-set state description of the semantics node. For example: on/off. If this not
@@ -420,16 +426,22 @@
* [AccessibilityRangeInfo], but it is not guaranteed and the format will be decided by
* accessibility services.
*
- * @see SemanticsProperties.AccessibilityValue
+ * @see SemanticsProperties.StateDescription
*/
-var SemanticsPropertyReceiver.accessibilityValue by SemanticsProperties.AccessibilityValue
+var SemanticsPropertyReceiver.stateDescription by SemanticsProperties.StateDescription
+
+@Deprecated(
+ "accessibilityValue was renamed to stateDescription",
+ ReplaceWith("stateDescription", "androidx.compose.ui.semantics")
+)
+var SemanticsPropertyReceiver.accessibilityValue by SemanticsProperties.StateDescription
/**
* The node is a range with current value.
*
* @see SemanticsProperties.AccessibilityRangeInfo
*/
-var SemanticsPropertyReceiver.accessibilityValueRange by SemanticsProperties.AccessibilityRangeInfo
+var SemanticsPropertyReceiver.stateDescriptionRange by SemanticsProperties.AccessibilityRangeInfo
/**
* Whether this semantics node is disabled.
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt
index f4131a1..872ad8b 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt
@@ -19,7 +19,6 @@
import androidx.compose.runtime.Providers
import androidx.compose.runtime.ambientOf
import androidx.compose.runtime.emptyContent
-import androidx.compose.ui.input.key.ExperimentalKeyInput
import androidx.compose.ui.platform.Keyboard
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
@@ -335,7 +334,6 @@
*
* @param content Composable content of the window.
*/
- @OptIn(ExperimentalKeyInput::class)
override fun show(content: @Composable () -> Unit) {
if (invoker != null) {
invoker!!.lockWindow()
@@ -387,6 +385,5 @@
/**
* Gets the Keyboard object of the window.
*/
- @ExperimentalKeyInput
val keyboard: Keyboard = Keyboard()
}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/key/KeyEventDesktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/key/KeyEventDesktop.kt
index c8aeb01..ed20405 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/key/KeyEventDesktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/key/KeyEventDesktop.kt
@@ -20,7 +20,6 @@
import java.awt.event.KeyEvent.KEY_RELEASED
import java.awt.event.KeyEvent as KeyEventAwt
-@OptIn(ExperimentalKeyInput::class)
internal inline class KeyEventDesktop(val keyEvent: KeyEventAwt) : KeyEvent {
override val key: Key
@@ -54,7 +53,6 @@
}
@Suppress("DEPRECATION")
-@OptIn(ExperimentalKeyInput::class)
internal inline class AltDesktop(val keyEvent: KeyEventAwt) : Alt {
override val isLeftAltPressed
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/key/ShortcutsModifier.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/key/ShortcutsModifier.kt
index 0dad286..9be1321 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/key/ShortcutsModifier.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/key/ShortcutsModifier.kt
@@ -80,7 +80,6 @@
private fun makeHandlers() = TreeMap<KeysSet, () -> Unit>()
-@ExperimentalKeyInput
internal class ShortcutsInstance(
internal var handlers: TreeMap<KeysSet, () -> Unit> = makeHandlers()
) {
@@ -133,7 +132,6 @@
* @see [keyInputFilter]
* @see [androidx.compose.ui.platform.Keyboard] to define window-scoped shortcuts
*/
-@ExperimentalKeyInput
@Composable
fun Modifier.shortcuts(builder: (ShortcutsBuilderScope).() -> Unit) = composed {
val instance = remember { ShortcutsInstance() }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
index 3510b9c..36913bb 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
@@ -24,13 +24,12 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.autofill.Autofill
import androidx.compose.ui.autofill.AutofillTree
-import androidx.compose.ui.focus.ExperimentalFocus
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.FocusManagerImpl
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.DesktopCanvas
-import androidx.compose.ui.input.key.ExperimentalKeyInput
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyInputModifier
import androidx.compose.ui.input.mouse.MouseScrollEvent
@@ -57,8 +56,7 @@
import androidx.compose.ui.unit.LayoutDirection
@OptIn(
- ExperimentalFocus::class,
- ExperimentalKeyInput::class,
+ ExperimentalComposeUiApi::class,
ExperimentalComposeApi::class,
InternalCoreApi::class
)
@@ -177,9 +175,6 @@
}
}
- override val hasPendingMeasureOrLayout
- get() = measureAndLayoutDelegate.hasPendingMeasureOrLayout
-
override fun createLayer(
drawBlock: (Canvas) -> Unit,
invalidateParentLayer: () -> Unit
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.kt
index d615e5e..5abcf8a 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.kt
@@ -18,7 +18,6 @@
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.staticAmbientOf
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.key.ExperimentalKeyInput
import androidx.compose.ui.input.key.KeyEventDesktop
import androidx.compose.ui.input.mouse.MouseScrollEvent
import androidx.compose.ui.input.pointer.PointerId
@@ -49,7 +48,6 @@
}
val list = LinkedHashSet<DesktopOwner>()
- @ExperimentalKeyInput
var keyboard: Keyboard? = null
private var pointerId = 0L
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelection.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelection.kt
index 1c150ec..e9fa62c 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelection.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelection.kt
@@ -22,7 +22,6 @@
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.gesture.DragObserver
import androidx.compose.ui.gesture.rawDragGestureFilter
@@ -91,7 +90,6 @@
}
}
-@OptIn(ExperimentalFocus::class)
@Composable
fun DesktopSelectionContainer(
selection: Selection?,
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionManager.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionManager.kt
index 8cf64c5..ea143f0 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionManager.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionManager.kt
@@ -25,7 +25,9 @@
import androidx.compose.ui.selection.getCurrentSelectedText
import androidx.compose.ui.selection.merge
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
+@OptIn(ExperimentalTextApi::class)
internal class DesktopSelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
private var dragBeginPosition = Offset.Zero
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionRegistrar.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionRegistrar.kt
index ce9ba4b4..71eae61 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionRegistrar.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionRegistrar.kt
@@ -20,8 +20,10 @@
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.selection.Selectable
import androidx.compose.ui.selection.SelectionRegistrar
+import androidx.compose.ui.text.ExperimentalTextApi
// based on androidx.compose.ui.selection.SelectionRegistrarImpl
+@OptIn(ExperimentalTextApi::class)
internal class DesktopSelectionRegistrar : SelectionRegistrar {
internal var sorted: Boolean = false
@@ -71,12 +73,23 @@
return selectables
}
- override fun onPositionChange() {
+ override fun notifyPositionChange() {
sorted = false
onPositionChangeCallback?.invoke()
}
- override fun onUpdateSelection(
+ override fun notifySelectionUpdateStart(
+ layoutCoordinates: LayoutCoordinates,
+ startPosition: Offset
+ ) {
+ onUpdateSelectionCallback?.invoke(
+ layoutCoordinates,
+ startPosition,
+ startPosition
+ )
+ }
+
+ override fun notifySelectionUpdate(
layoutCoordinates: LayoutCoordinates,
startPosition: Offset,
endPosition: Offset
@@ -87,4 +100,6 @@
endPosition
)
}
+
+ override fun notifySelectionUpdateEnd() { /* do nothing */ }
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopUiApplier.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopUiApplier.kt
index 0e0e99e..6fea479 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopUiApplier.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopUiApplier.kt
@@ -24,7 +24,11 @@
internal class DesktopUiApplier(
root: LayoutNode
) : AbstractApplier<LayoutNode>(root) {
- override fun insert(index: Int, instance: LayoutNode) {
+ override fun insertTopDown(index: Int, instance: LayoutNode) {
+ // ignored. Building tree bottom-up
+ }
+
+ override fun insertBottomUp(index: Int, instance: LayoutNode) {
current.insertAt(index, instance)
}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/Keyboard.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/Keyboard.kt
index 750baf3..95b1628 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/Keyboard.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/Keyboard.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.platform
-import androidx.compose.ui.input.key.ExperimentalKeyInput
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeysSet
@@ -28,7 +27,6 @@
*
* @see [shortcuts] to setup event handlers based on the element that is in focus
*/
-@ExperimentalKeyInput
class Keyboard {
private val shortcutsInstance = lazy {
ShortcutsInstance()
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/Wrapper.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/Wrapper.kt
index 9c7e461..c94e69d 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/Wrapper.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/Wrapper.kt
@@ -22,10 +22,10 @@
import androidx.compose.runtime.Providers
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.compositionFor
-import androidx.compose.ui.input.key.ExperimentalKeyInput
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.node.LayoutNode
-@OptIn(ExperimentalComposeApi::class, ExperimentalKeyInput::class)
+@OptIn(ExperimentalComposeApi::class)
fun DesktopOwner.setContent(content: @Composable () -> Unit): Composition {
GlobalSnapshotManager.ensureStarted()
@@ -46,8 +46,7 @@
return composition
}
-
-@OptIn(ExperimentalKeyInput::class)
+@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun ProvideDesktopAmbients(owner: DesktopOwner, content: @Composable () -> Unit) {
Providers(
@@ -64,7 +63,7 @@
}
@OptIn(ExperimentalComposeApi::class)
-internal actual fun actualSubcomposeInto(
+internal actual fun subcomposeInto(
container: LayoutNode,
parent: CompositionReference,
composable: @Composable () -> Unit
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/key/KeyInputUtil.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/key/KeyInputUtil.kt
index 0429c05..280e4817 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/key/KeyInputUtil.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/key/KeyInputUtil.kt
@@ -28,7 +28,6 @@
* The [KeyEvent] is usually created by the system. This function creates an instance of
* [KeyEvent] that can be used in tests.
*/
-@OptIn(ExperimentalKeyInput::class)
fun keyEvent(key: Key, keyEventType: KeyEventType): KeyEvent {
val action = when (keyEventType) {
KeyEventType.KeyDown -> KEY_PRESSED
@@ -46,7 +45,6 @@
/**
* Creates [KeyEvent] of Unknown type. It wraps KEY_TYPED AWTs KeyEvent
*/
-@OptIn(ExperimentalKeyInput::class)
fun keyTypedEvent(key: Key): KeyEvent {
return KeyEventDesktop(
KeyEventAwt(
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/key/ShortcutsTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/key/ShortcutsTest.kt
index 78df8af..0073746 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/key/ShortcutsTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/key/ShortcutsTest.kt
@@ -23,7 +23,6 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focusRequester
import androidx.compose.ui.test.junit4.createComposeRule
@@ -37,10 +36,6 @@
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
-@OptIn(
- ExperimentalFocus::class,
- ExperimentalKeyInput::class
-)
class ShortcutsTest {
@get:Rule
val rule = createComposeRule()
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/mouse/MouseScrollFilterTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/mouse/MouseScrollFilterTest.kt
index 1fed94d..d743d95 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/mouse/MouseScrollFilterTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/mouse/MouseScrollFilterTest.kt
@@ -146,7 +146,7 @@
)
Box(
Modifier
- .mouseScrollFilter { event, bounds ->
+ .mouseScrollFilter { _, _ ->
false
}
.size(5.dp, 10.dp)
@@ -254,7 +254,7 @@
) {
Box(
Modifier
- .mouseScrollFilter { event, bounds ->
+ .mouseScrollFilter { _, _ ->
false
}
.size(5.dp, 10.dp)
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt
index 87ad037..7f0aca2 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt
@@ -29,7 +29,7 @@
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyColumnFor
+import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -53,7 +53,6 @@
import org.junit.Test
@OptIn(
- ExperimentalLayoutNodeApi::class,
ExperimentalComposeApi::class,
ExperimentalCoroutinesApi::class
)
@@ -289,10 +288,12 @@
var height by mutableStateOf(10.dp)
setContent {
Box(Modifier.padding(10.dp)) {
- LazyColumnFor(
- listOf(Color.Red, Color.Green, Color.Blue, Color.Black, Color.Gray)
- ) { color ->
- Box(Modifier.size(width = 30.dp, height = height).background(color))
+ LazyColumn {
+ items(
+ listOf(Color.Red, Color.Green, Color.Blue, Color.Black, Color.Gray)
+ ) { color ->
+ Box(Modifier.size(width = 30.dp, height = height).background(color))
+ }
}
}
}
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/ComposedModifierTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/ComposedModifierTest.kt
index 0a4c2b3..7485666 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/ComposedModifierTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/ComposedModifierTest.kt
@@ -241,7 +241,10 @@
override val current: Unit = Unit
override fun down(node: Unit) {}
override fun up() {}
- override fun insert(index: Int, instance: Unit) {
+ override fun insertTopDown(index: Int, instance: Unit) {
+ error("Unexpected")
+ }
+ override fun insertBottomUp(index: Int, instance: Unit) {
error("Unexpected")
}
override fun remove(index: Int, count: Int) {
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusObserverModifierTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusObserverModifierTest.kt
index 682eed0..4684ec3 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusObserverModifierTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusObserverModifierTest.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.ValueElement
@@ -37,7 +36,6 @@
isDebugInspectorInfoEnabled = false
}
- @OptIn(ExperimentalFocus::class)
@Test
fun testInspectorValue() {
val onFocusChange: (FocusState) -> Unit = {}
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusRequesterModifierTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusRequesterModifierTest.kt
index d54d1d1..f8ae9fa 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusRequesterModifierTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/FocusRequesterModifierTest.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.ValueElement
@@ -37,7 +36,6 @@
isDebugInspectorInfoEnabled = false
}
- @OptIn(ExperimentalFocus::class)
@Test
fun testInspectorValue() {
val focusRequester = FocusRequester()
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidAutofillTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidAutofillTest.kt
index 3cbb212..6ef2547 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidAutofillTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidAutofillTest.kt
@@ -19,6 +19,7 @@
import android.app.Activity
import android.view.View
import android.view.autofill.AutofillManager
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.toComposeRect
import androidx.compose.ui.test.ComposeUiRobolectricTestRunner
@@ -35,6 +36,7 @@
import org.robolectric.shadow.api.Shadow
import android.graphics.Rect as AndroidRect
+@OptIn(ExperimentalComposeUiApi::class)
@RunWith(ComposeUiRobolectricTestRunner::class)
@Config(
shadows = [ShadowAutofillManager::class],
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidAutofillTypeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidAutofillTypeTest.kt
index 0475d44..4361ec1 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidAutofillTypeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidAutofillTypeTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.autofill
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.autofill.AutofillType.AddressAuxiliaryDetails
import androidx.compose.ui.autofill.AutofillType.AddressCountry
import androidx.compose.ui.autofill.AutofillType.AddressLocality
@@ -57,6 +58,7 @@
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+@OptIn(ExperimentalComposeUiApi::class)
@RunWith(JUnit4::class)
class AndroidAutofillTypeTest {
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidPerformAutofillTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidPerformAutofillTest.kt
index 4cb174c..f223b15 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidPerformAutofillTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidPerformAutofillTest.kt
@@ -20,6 +20,7 @@
import android.util.SparseArray
import android.view.View
import android.view.autofill.AutofillValue
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.test.ComposeUiRobolectricTestRunner
import com.google.common.truth.Truth
@@ -29,6 +30,7 @@
import org.robolectric.Robolectric
import org.robolectric.annotation.Config
+@OptIn(ExperimentalComposeUiApi::class)
@RunWith(ComposeUiRobolectricTestRunner::class)
@Config(minSdk = 26)
class AndroidPerformAutofillTest {
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidPopulateViewStructureTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidPopulateViewStructureTest.kt
index 1653cf7..499687b 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidPopulateViewStructureTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AndroidPopulateViewStructureTest.kt
@@ -21,6 +21,7 @@
import android.view.ViewStructure
import androidx.autofill.HintConstants.AUTOFILL_HINT_PERSON_NAME
import androidx.compose.testutils.fake.FakeViewStructure
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.test.ComposeUiRobolectricTestRunner
import com.google.common.truth.Truth.assertThat
@@ -30,6 +31,7 @@
import org.robolectric.Robolectric
import org.robolectric.annotation.Config
+@OptIn(ExperimentalComposeUiApi::class)
@RunWith(ComposeUiRobolectricTestRunner::class)
@Config(
manifest = Config.NONE,
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AutofillNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AutofillNodeTest.kt
index c56ffcc..42294c6 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AutofillNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/autofill/AutofillNodeTest.kt
@@ -16,11 +16,13 @@
package androidx.compose.ui.autofill
+import androidx.compose.ui.ExperimentalComposeUiApi
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+@OptIn(ExperimentalComposeUiApi::class)
@RunWith(JUnit4::class)
class AutofillNodeTest {
@Test
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusManagerTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusManagerTest.kt
index 4caf023..365dcd3 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusManagerTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusManagerTest.kt
@@ -32,9 +32,6 @@
import org.junit.runners.Parameterized
import kotlin.jvm.JvmStatic
-@OptIn(
- ExperimentalFocus::class,
-)
@RunWith(Parameterized::class)
class FocusManagerTest(private val initialFocusState: FocusState) {
companion object {
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/DoubleTapGestureFilterTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/DoubleTapGestureFilterTest.kt
index 20cffbc..3d5fc83 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/DoubleTapGestureFilterTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/DoubleTapGestureFilterTest.kt
@@ -14,8 +14,6 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalPointerInput::class)
-
package androidx.compose.ui.gesture
import androidx.compose.ui.Modifier
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/DragSlopExceededGestureFilterTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/DragSlopExceededGestureFilterTest.kt
index 778a789..f45f5c0 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/DragSlopExceededGestureFilterTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/DragSlopExceededGestureFilterTest.kt
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalPointerInput::class)
@file:Suppress("PrivatePropertyName")
package androidx.compose.ui.gesture
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/TapGestureFilterTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/TapGestureFilterTest.kt
index 9d686f0..658136a 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/TapGestureFilterTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/TapGestureFilterTest.kt
@@ -14,8 +14,6 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalPointerInput::class)
-
package androidx.compose.ui.gesture
import androidx.compose.ui.Modifier
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLockerSetupTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLockerSetupTest.kt
index b70b2b6..70abfca 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLockerSetupTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLockerSetupTest.kt
@@ -14,11 +14,8 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalPointerInput::class)
-
package androidx.compose.ui.gesture.scrollorientationlocking
-import androidx.compose.ui.gesture.ExperimentalPointerInput
import androidx.compose.ui.input.pointer.CustomEvent
import androidx.compose.ui.input.pointer.CustomEventDispatcher
import androidx.compose.ui.input.pointer.PointerEventPass
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLockerTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLockerTest.kt
index 8d26027..37139367 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLockerTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/gesture/scrollorientationlocking/ScrollOrientationLockerTest.kt
@@ -14,11 +14,8 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalPointerInput::class)
-
package androidx.compose.ui.gesture.scrollorientationlocking
-import androidx.compose.ui.gesture.ExperimentalPointerInput
import androidx.compose.ui.input.pointer.CustomEventDispatcher
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerInputChange
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/key/KeyInputModifierTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/key/KeyInputModifierTest.kt
index d072a85..75e35df 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/key/KeyInputModifierTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/key/KeyInputModifierTest.kt
@@ -36,7 +36,6 @@
isDebugInspectorInfoEnabled = false
}
- @OptIn(ExperimentalKeyInput::class)
@Test
fun testInspectorValueForKeyInputFilter() {
val onKeyEvent: (KeyEvent) -> Boolean = { true }
@@ -48,7 +47,6 @@
)
}
- @OptIn(ExperimentalKeyInput::class)
@Test
fun testInspectorValueForPreviewKeyInputFilter() {
val onPreviewKeyEvent: (KeyEvent) -> Boolean = { true }
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index 625a6d3..825ef67 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -15,6 +15,7 @@
*/
package androidx.compose.ui.node
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.draw.DrawModifier
import androidx.compose.ui.Modifier
@@ -22,7 +23,6 @@
import androidx.compose.ui.autofill.Autofill
import androidx.compose.ui.autofill.AutofillTree
import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
@@ -30,7 +30,6 @@
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.hapticfeedback.HapticFeedback
-import androidx.compose.ui.input.key.ExperimentalKeyInput
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.pointer.PointerInputFilter
import androidx.compose.ui.input.pointer.PointerInputModifier
@@ -38,6 +37,7 @@
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.TextToolbar
@@ -79,13 +79,13 @@
val owner = MockOwner()
node.attach(owner)
assertEquals(owner, node.owner)
- assertTrue(node.isAttached())
+ assertTrue(node.isAttached)
assertEquals(1, owner.onAttachParams.count { it === node })
node.detach()
assertNull(node.owner)
- assertFalse(node.isAttached())
+ assertFalse(node.isAttached)
assertEquals(1, owner.onDetachParams.count { it === node })
}
@@ -1633,12 +1633,50 @@
// Dispose
root.removeAt(0, 1)
- assertFalse(node1.isAttached())
- assertFalse(node2.isAttached())
+ assertFalse(node1.isAttached)
+ assertFalse(node2.isAttached)
assertEquals(0, owner.onRequestMeasureParams.count { it === node1 })
assertEquals(0, owner.onRequestMeasureParams.count { it === node2 })
}
+ @Test
+ fun modifierMatchesWrapperWithIdentity() {
+ val modifier1 = Modifier.layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+ val modifier2 = Modifier.layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(1, 1)
+ }
+ }
+
+ val root = LayoutNode()
+ root.modifier = modifier1.then(modifier2)
+
+ val wrapper1 = root.outerLayoutNodeWrapper
+ val wrapper2 = root.outerLayoutNodeWrapper.wrapped
+
+ assertEquals(modifier1, (wrapper1 as DelegatingLayoutNodeWrapper<*>).modifier)
+ assertEquals(modifier2, (wrapper2 as DelegatingLayoutNodeWrapper<*>).modifier)
+
+ root.modifier = modifier2.then(modifier1)
+
+ assertEquals(wrapper2, root.outerLayoutNodeWrapper)
+ assertEquals(wrapper1, root.outerLayoutNodeWrapper.wrapped)
+ assertEquals(
+ modifier1,
+ (root.outerLayoutNodeWrapper.wrapped as DelegatingLayoutNodeWrapper<*>).modifier
+ )
+ assertEquals(
+ modifier2,
+ (root.outerLayoutNodeWrapper as DelegatingLayoutNodeWrapper<*>).modifier
+ )
+ }
+
private fun createSimpleLayout(): Triple<LayoutNode, LayoutNode, LayoutNode> {
val layoutNode = ZeroSizedLayoutNode()
val child1 = ZeroSizedLayoutNode()
@@ -1654,10 +1692,7 @@
PointerInputModifier
}
-@OptIn(
- ExperimentalFocus::class,
- InternalCoreApi::class
-)
+@OptIn(InternalCoreApi::class)
private class MockOwner(
val position: IntOffset = IntOffset.Zero,
override val root: LayoutNode = LayoutNode()
@@ -1672,8 +1707,10 @@
get() = TODO("Not yet implemented")
override val textToolbar: TextToolbar
get() = TODO("Not yet implemented")
+ @OptIn(ExperimentalComposeUiApi::class)
override val autofillTree: AutofillTree
get() = TODO("Not yet implemented")
+ @OptIn(ExperimentalComposeUiApi::class)
override val autofill: Autofill?
get() = TODO("Not yet implemented")
override val density: Density
@@ -1702,8 +1739,6 @@
layoutNode.layoutState = LayoutNode.LayoutState.NeedsRelayout
}
- override val hasPendingMeasureOrLayout = false
-
override fun onAttach(node: LayoutNode) {
onAttachParams += node
}
@@ -1716,7 +1751,6 @@
override fun requestFocus(): Boolean = false
- @ExperimentalKeyInput
override fun sendKeyEvent(keyEvent: KeyEvent): Boolean = false
override fun measureAndLayout() {
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/MockSelectable.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/MockSelectable.kt
index ede87ef..baa520a 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/MockSelectable.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/MockSelectable.kt
@@ -20,7 +20,9 @@
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
+@OptIn(ExperimentalTextApi::class)
class MockSelectable(
var getSelectionValue: Selection? = null,
var getHandlePositionValue: Offset = Offset.Zero,
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerDragTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerDragTest.kt
index 323adf5..b066747 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerDragTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerDragTest.kt
@@ -18,6 +18,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.style.ResolvedTextDirection
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.any
@@ -32,6 +33,7 @@
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+@OptIn(ExperimentalTextApi::class)
@RunWith(JUnit4::class)
class SelectionManagerDragTest {
private val selectionRegistrar = SelectionRegistrarImpl()
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerTest.kt
index ff70baa..18ce38e 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerTest.kt
@@ -24,6 +24,7 @@
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.length
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.text.subSequence
@@ -42,6 +43,7 @@
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+@OptIn(ExperimentalTextApi::class)
@RunWith(JUnit4::class)
class SelectionManagerTest {
private val selectionRegistrar = spy(SelectionRegistrarImpl())
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionRegistrarImplTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionRegistrarImplTest.kt
index 130db56..4bf9891a 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionRegistrarImplTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionRegistrarImplTest.kt
@@ -18,6 +18,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.ExperimentalTextApi
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
@@ -25,6 +26,7 @@
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+@OptIn(ExperimentalTextApi::class)
@RunWith(JUnit4::class)
class SelectionRegistrarImplTest {
@Test
@@ -188,7 +190,7 @@
assertThat(selectionRegistrar.sorted).isTrue()
// Act.
- selectionRegistrar.onPositionChange()
+ selectionRegistrar.notifyPositionChange()
// Assert.
assertThat(selectionRegistrar.sorted).isFalse()
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionTest.kt
index 5febaab..6afcab9 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.selection
+import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.style.ResolvedTextDirection
import com.google.common.truth.Truth.assertThat
@@ -24,6 +25,7 @@
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+@OptIn(ExperimentalTextApi::class)
@RunWith(JUnit4::class)
class SelectionTest {
@Test
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index 2e71858..21fe8d9 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -842,8 +842,8 @@
}
public final class ShareCompat {
- method public static void configureMenuItem(android.view.MenuItem, androidx.core.app.ShareCompat.IntentBuilder);
- method public static void configureMenuItem(android.view.Menu, @IdRes int, androidx.core.app.ShareCompat.IntentBuilder);
+ method @Deprecated public static void configureMenuItem(android.view.MenuItem, androidx.core.app.ShareCompat.IntentBuilder);
+ method @Deprecated public static void configureMenuItem(android.view.Menu, @IdRes int, androidx.core.app.ShareCompat.IntentBuilder);
method public static android.content.ComponentName? getCallingActivity(android.app.Activity);
method public static String? getCallingPackage(android.app.Activity);
field public static final String EXTRA_CALLING_ACTIVITY = "androidx.core.app.EXTRA_CALLING_ACTIVITY";
@@ -1524,7 +1524,7 @@
method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.O_MR1) public static boolean isAtLeastOMR1();
method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.P) public static boolean isAtLeastP();
method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.Q) public static boolean isAtLeastQ();
- method @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
+ method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
method @ChecksSdkIntAtLeast(codename="S") public static boolean isAtLeastS();
}
@@ -1912,6 +1912,30 @@
method public void onActionProviderVisibilityChanged(boolean);
}
+ public final class ContentInfoCompat {
+ method public android.content.ClipData getClip();
+ method public android.os.Bundle? getExtras();
+ method public int getFlags();
+ method public android.net.Uri? getLinkUri();
+ method public int getSource();
+ method public android.util.Pair<androidx.core.view.ContentInfoCompat!,androidx.core.view.ContentInfoCompat!> partition(androidx.core.util.Predicate<android.content.ClipData.Item!>);
+ field public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1; // 0x1
+ field public static final int SOURCE_APP = 0; // 0x0
+ field public static final int SOURCE_CLIPBOARD = 1; // 0x1
+ field public static final int SOURCE_INPUT_METHOD = 2; // 0x2
+ }
+
+ public static final class ContentInfoCompat.Builder {
+ ctor public ContentInfoCompat.Builder(androidx.core.view.ContentInfoCompat);
+ ctor public ContentInfoCompat.Builder(android.content.ClipData, int);
+ method public androidx.core.view.ContentInfoCompat build();
+ method public androidx.core.view.ContentInfoCompat.Builder setClip(android.content.ClipData);
+ method public androidx.core.view.ContentInfoCompat.Builder setExtras(android.os.Bundle?);
+ method public androidx.core.view.ContentInfoCompat.Builder setFlags(int);
+ method public androidx.core.view.ContentInfoCompat.Builder setLinkUri(android.net.Uri?);
+ method public androidx.core.view.ContentInfoCompat.Builder setSource(int);
+ }
+
public final class DisplayCompat {
method public static androidx.core.view.DisplayCompat.ModeCompat![] getSupportedModes(android.content.Context, android.view.Display);
}
@@ -2208,6 +2232,14 @@
method public androidx.core.view.WindowInsetsCompat! onApplyWindowInsets(android.view.View!, androidx.core.view.WindowInsetsCompat!);
}
+ public interface OnReceiveContentListener {
+ method public androidx.core.view.ContentInfoCompat? onReceiveContent(android.view.View, androidx.core.view.ContentInfoCompat);
+ }
+
+ public interface OnReceiveContentViewBehavior {
+ method public androidx.core.view.ContentInfoCompat? onReceiveContent(androidx.core.view.ContentInfoCompat);
+ }
+
public final class OneShotPreDrawListener implements android.view.View.OnAttachStateChangeListener android.view.ViewTreeObserver.OnPreDrawListener {
method public static androidx.core.view.OneShotPreDrawListener add(android.view.View, Runnable);
method public boolean onPreDraw();
@@ -2319,6 +2351,7 @@
method public static int getMinimumHeight(android.view.View);
method public static int getMinimumWidth(android.view.View);
method public static int getNextClusterForwardId(android.view.View);
+ method public static String![]? getOnReceiveContentMimeTypes(android.view.View);
method @Deprecated public static int getOverScrollMode(android.view.View!);
method @Px public static int getPaddingEnd(android.view.View);
method @Px public static int getPaddingStart(android.view.View);
@@ -2372,6 +2405,7 @@
method public static void onInitializeAccessibilityNodeInfo(android.view.View, androidx.core.view.accessibility.AccessibilityNodeInfoCompat!);
method @Deprecated public static void onPopulateAccessibilityEvent(android.view.View!, android.view.accessibility.AccessibilityEvent!);
method public static boolean performAccessibilityAction(android.view.View, int, android.os.Bundle!);
+ method public static androidx.core.view.ContentInfoCompat? performReceiveContent(android.view.View, androidx.core.view.ContentInfoCompat);
method public static void postInvalidateOnAnimation(android.view.View);
method public static void postInvalidateOnAnimation(android.view.View, int, int, int, int);
method public static void postOnAnimation(android.view.View, Runnable!);
@@ -2410,6 +2444,7 @@
method public static void setNestedScrollingEnabled(android.view.View, boolean);
method public static void setNextClusterForwardId(android.view.View, int);
method public static void setOnApplyWindowInsetsListener(android.view.View, androidx.core.view.OnApplyWindowInsetsListener?);
+ method public static void setOnReceiveContentListener(android.view.View, String![]?, androidx.core.view.OnReceiveContentListener?);
method @Deprecated public static void setOverScrollMode(android.view.View!, int);
method public static void setPaddingRelative(android.view.View, @Px int, @Px int, @Px int, @Px int);
method @Deprecated public static void setPivotX(android.view.View!, float);
@@ -3335,15 +3370,6 @@
method public static void showAsDropDown(android.widget.PopupWindow, android.view.View, int, int, int);
}
- public abstract class RichContentReceiverCompat<T extends android.view.View> {
- ctor public RichContentReceiverCompat();
- method public abstract java.util.Set<java.lang.String!> getSupportedMimeTypes();
- method public abstract boolean onReceive(T, android.content.ClipData, int, int);
- field public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1; // 0x1
- field public static final int SOURCE_CLIPBOARD = 0; // 0x0
- field public static final int SOURCE_INPUT_METHOD = 1; // 0x1
- }
-
@Deprecated public final class ScrollerCompat {
method @Deprecated public void abortAnimation();
method @Deprecated public boolean computeScrollOffset();
@@ -3398,12 +3424,6 @@
field public static final int AUTO_SIZE_TEXT_TYPE_UNIFORM = 1; // 0x1
}
- public abstract class TextViewRichContentReceiverCompat extends androidx.core.widget.RichContentReceiverCompat<android.widget.TextView> {
- ctor public TextViewRichContentReceiverCompat();
- method public java.util.Set<java.lang.String!> getSupportedMimeTypes();
- method public boolean onReceive(android.widget.TextView, android.content.ClipData, int, int);
- }
-
public interface TintableCompoundButton {
method public android.content.res.ColorStateList? getSupportButtonTintList();
method public android.graphics.PorterDuff.Mode? getSupportButtonTintMode();
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index 8c72f00..c23381b 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -842,8 +842,8 @@
}
public final class ShareCompat {
- method public static void configureMenuItem(android.view.MenuItem, androidx.core.app.ShareCompat.IntentBuilder);
- method public static void configureMenuItem(android.view.Menu, @IdRes int, androidx.core.app.ShareCompat.IntentBuilder);
+ method @Deprecated public static void configureMenuItem(android.view.MenuItem, androidx.core.app.ShareCompat.IntentBuilder);
+ method @Deprecated public static void configureMenuItem(android.view.Menu, @IdRes int, androidx.core.app.ShareCompat.IntentBuilder);
method public static android.content.ComponentName? getCallingActivity(android.app.Activity);
method public static String? getCallingPackage(android.app.Activity);
field public static final String EXTRA_CALLING_ACTIVITY = "androidx.core.app.EXTRA_CALLING_ACTIVITY";
@@ -1522,7 +1522,7 @@
method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.O_MR1) public static boolean isAtLeastOMR1();
method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.P) public static boolean isAtLeastP();
method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.Q) public static boolean isAtLeastQ();
- method @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
+ method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
method @ChecksSdkIntAtLeast(codename="S") public static boolean isAtLeastS();
}
@@ -1910,6 +1910,30 @@
method public void onActionProviderVisibilityChanged(boolean);
}
+ public final class ContentInfoCompat {
+ method public android.content.ClipData getClip();
+ method public android.os.Bundle? getExtras();
+ method public int getFlags();
+ method public android.net.Uri? getLinkUri();
+ method public int getSource();
+ method public android.util.Pair<androidx.core.view.ContentInfoCompat!,androidx.core.view.ContentInfoCompat!> partition(androidx.core.util.Predicate<android.content.ClipData.Item!>);
+ field public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1; // 0x1
+ field public static final int SOURCE_APP = 0; // 0x0
+ field public static final int SOURCE_CLIPBOARD = 1; // 0x1
+ field public static final int SOURCE_INPUT_METHOD = 2; // 0x2
+ }
+
+ public static final class ContentInfoCompat.Builder {
+ ctor public ContentInfoCompat.Builder(androidx.core.view.ContentInfoCompat);
+ ctor public ContentInfoCompat.Builder(android.content.ClipData, int);
+ method public androidx.core.view.ContentInfoCompat build();
+ method public androidx.core.view.ContentInfoCompat.Builder setClip(android.content.ClipData);
+ method public androidx.core.view.ContentInfoCompat.Builder setExtras(android.os.Bundle?);
+ method public androidx.core.view.ContentInfoCompat.Builder setFlags(int);
+ method public androidx.core.view.ContentInfoCompat.Builder setLinkUri(android.net.Uri?);
+ method public androidx.core.view.ContentInfoCompat.Builder setSource(int);
+ }
+
public final class DisplayCompat {
method public static androidx.core.view.DisplayCompat.ModeCompat![] getSupportedModes(android.content.Context, android.view.Display);
}
@@ -2206,6 +2230,14 @@
method public androidx.core.view.WindowInsetsCompat! onApplyWindowInsets(android.view.View!, androidx.core.view.WindowInsetsCompat!);
}
+ public interface OnReceiveContentListener {
+ method public androidx.core.view.ContentInfoCompat? onReceiveContent(android.view.View, androidx.core.view.ContentInfoCompat);
+ }
+
+ public interface OnReceiveContentViewBehavior {
+ method public androidx.core.view.ContentInfoCompat? onReceiveContent(androidx.core.view.ContentInfoCompat);
+ }
+
public final class OneShotPreDrawListener implements android.view.View.OnAttachStateChangeListener android.view.ViewTreeObserver.OnPreDrawListener {
method public static androidx.core.view.OneShotPreDrawListener add(android.view.View, Runnable);
method public boolean onPreDraw();
@@ -2317,6 +2349,7 @@
method public static int getMinimumHeight(android.view.View);
method public static int getMinimumWidth(android.view.View);
method public static int getNextClusterForwardId(android.view.View);
+ method public static String![]? getOnReceiveContentMimeTypes(android.view.View);
method @Deprecated public static int getOverScrollMode(android.view.View!);
method @Px public static int getPaddingEnd(android.view.View);
method @Px public static int getPaddingStart(android.view.View);
@@ -2370,6 +2403,7 @@
method public static void onInitializeAccessibilityNodeInfo(android.view.View, androidx.core.view.accessibility.AccessibilityNodeInfoCompat!);
method @Deprecated public static void onPopulateAccessibilityEvent(android.view.View!, android.view.accessibility.AccessibilityEvent!);
method public static boolean performAccessibilityAction(android.view.View, int, android.os.Bundle!);
+ method public static androidx.core.view.ContentInfoCompat? performReceiveContent(android.view.View, androidx.core.view.ContentInfoCompat);
method public static void postInvalidateOnAnimation(android.view.View);
method public static void postInvalidateOnAnimation(android.view.View, int, int, int, int);
method public static void postOnAnimation(android.view.View, Runnable!);
@@ -2408,6 +2442,7 @@
method public static void setNestedScrollingEnabled(android.view.View, boolean);
method public static void setNextClusterForwardId(android.view.View, int);
method public static void setOnApplyWindowInsetsListener(android.view.View, androidx.core.view.OnApplyWindowInsetsListener?);
+ method public static void setOnReceiveContentListener(android.view.View, String![]?, androidx.core.view.OnReceiveContentListener?);
method @Deprecated public static void setOverScrollMode(android.view.View!, int);
method public static void setPaddingRelative(android.view.View, @Px int, @Px int, @Px int, @Px int);
method @Deprecated public static void setPivotX(android.view.View!, float);
@@ -3333,15 +3368,6 @@
method public static void showAsDropDown(android.widget.PopupWindow, android.view.View, int, int, int);
}
- public abstract class RichContentReceiverCompat<T extends android.view.View> {
- ctor public RichContentReceiverCompat();
- method public abstract java.util.Set<java.lang.String!> getSupportedMimeTypes();
- method public abstract boolean onReceive(T, android.content.ClipData, int, int);
- field public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1; // 0x1
- field public static final int SOURCE_CLIPBOARD = 0; // 0x0
- field public static final int SOURCE_INPUT_METHOD = 1; // 0x1
- }
-
@Deprecated public final class ScrollerCompat {
method @Deprecated public void abortAnimation();
method @Deprecated public boolean computeScrollOffset();
@@ -3396,12 +3422,6 @@
field public static final int AUTO_SIZE_TEXT_TYPE_UNIFORM = 1; // 0x1
}
- public abstract class TextViewRichContentReceiverCompat extends androidx.core.widget.RichContentReceiverCompat<android.widget.TextView> {
- ctor public TextViewRichContentReceiverCompat();
- method public java.util.Set<java.lang.String!> getSupportedMimeTypes();
- method public boolean onReceive(android.widget.TextView, android.content.ClipData, int, int);
- }
-
public interface TintableCompoundButton {
method public android.content.res.ColorStateList? getSupportButtonTintList();
method public android.graphics.PorterDuff.Mode? getSupportButtonTintMode();
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 729b498..25fce0b 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -945,8 +945,8 @@
}
public final class ShareCompat {
- method public static void configureMenuItem(android.view.MenuItem, androidx.core.app.ShareCompat.IntentBuilder);
- method public static void configureMenuItem(android.view.Menu, @IdRes int, androidx.core.app.ShareCompat.IntentBuilder);
+ method @Deprecated public static void configureMenuItem(android.view.MenuItem, androidx.core.app.ShareCompat.IntentBuilder);
+ method @Deprecated public static void configureMenuItem(android.view.Menu, @IdRes int, androidx.core.app.ShareCompat.IntentBuilder);
method public static android.content.ComponentName? getCallingActivity(android.app.Activity);
method public static String? getCallingPackage(android.app.Activity);
field public static final String EXTRA_CALLING_ACTIVITY = "androidx.core.app.EXTRA_CALLING_ACTIVITY";
@@ -1834,7 +1834,7 @@
method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.O_MR1) public static boolean isAtLeastOMR1();
method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.P) public static boolean isAtLeastP();
method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.Q) public static boolean isAtLeastQ();
- method @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
+ method @Deprecated @ChecksSdkIntAtLeast(api=android.os.Build.VERSION_CODES.R) public static boolean isAtLeastR();
method @ChecksSdkIntAtLeast(codename="S") public static boolean isAtLeastS();
}
@@ -2296,6 +2296,36 @@
method public void onActionProviderVisibilityChanged(boolean);
}
+ public final class ContentInfoCompat {
+ method public android.content.ClipData getClip();
+ method public android.os.Bundle? getExtras();
+ method @androidx.core.view.ContentInfoCompat.Flags public int getFlags();
+ method public android.net.Uri? getLinkUri();
+ method @androidx.core.view.ContentInfoCompat.Source public int getSource();
+ method public android.util.Pair<androidx.core.view.ContentInfoCompat!,androidx.core.view.ContentInfoCompat!> partition(androidx.core.util.Predicate<android.content.ClipData.Item!>);
+ field public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1; // 0x1
+ field public static final int SOURCE_APP = 0; // 0x0
+ field public static final int SOURCE_CLIPBOARD = 1; // 0x1
+ field public static final int SOURCE_INPUT_METHOD = 2; // 0x2
+ }
+
+ public static final class ContentInfoCompat.Builder {
+ ctor public ContentInfoCompat.Builder(androidx.core.view.ContentInfoCompat);
+ ctor public ContentInfoCompat.Builder(android.content.ClipData, @androidx.core.view.ContentInfoCompat.Source int);
+ method public androidx.core.view.ContentInfoCompat build();
+ method public androidx.core.view.ContentInfoCompat.Builder setClip(android.content.ClipData);
+ method public androidx.core.view.ContentInfoCompat.Builder setExtras(android.os.Bundle?);
+ method public androidx.core.view.ContentInfoCompat.Builder setFlags(@androidx.core.view.ContentInfoCompat.Flags int);
+ method public androidx.core.view.ContentInfoCompat.Builder setLinkUri(android.net.Uri?);
+ method public androidx.core.view.ContentInfoCompat.Builder setSource(@androidx.core.view.ContentInfoCompat.Source int);
+ }
+
+ @IntDef(flag=true, value={androidx.core.view.ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ContentInfoCompat.Flags {
+ }
+
+ @IntDef({androidx.core.view.ContentInfoCompat.SOURCE_APP, androidx.core.view.ContentInfoCompat.SOURCE_CLIPBOARD, androidx.core.view.ContentInfoCompat.SOURCE_INPUT_METHOD}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ContentInfoCompat.Source {
+ }
+
public final class DisplayCompat {
method public static androidx.core.view.DisplayCompat.ModeCompat![] getSupportedModes(android.content.Context, android.view.Display);
}
@@ -2602,6 +2632,14 @@
method public androidx.core.view.WindowInsetsCompat! onApplyWindowInsets(android.view.View!, androidx.core.view.WindowInsetsCompat!);
}
+ public interface OnReceiveContentListener {
+ method public androidx.core.view.ContentInfoCompat? onReceiveContent(android.view.View, androidx.core.view.ContentInfoCompat);
+ }
+
+ public interface OnReceiveContentViewBehavior {
+ method public androidx.core.view.ContentInfoCompat? onReceiveContent(androidx.core.view.ContentInfoCompat);
+ }
+
public final class OneShotPreDrawListener implements android.view.View.OnAttachStateChangeListener android.view.ViewTreeObserver.OnPreDrawListener {
method public static androidx.core.view.OneShotPreDrawListener add(android.view.View, Runnable);
method public boolean onPreDraw();
@@ -2714,6 +2752,7 @@
method public static int getMinimumHeight(android.view.View);
method public static int getMinimumWidth(android.view.View);
method public static int getNextClusterForwardId(android.view.View);
+ method public static String![]? getOnReceiveContentMimeTypes(android.view.View);
method @Deprecated public static int getOverScrollMode(android.view.View!);
method @Px public static int getPaddingEnd(android.view.View);
method @Px public static int getPaddingStart(android.view.View);
@@ -2767,6 +2806,7 @@
method public static void onInitializeAccessibilityNodeInfo(android.view.View, androidx.core.view.accessibility.AccessibilityNodeInfoCompat!);
method @Deprecated public static void onPopulateAccessibilityEvent(android.view.View!, android.view.accessibility.AccessibilityEvent!);
method public static boolean performAccessibilityAction(android.view.View, int, android.os.Bundle!);
+ method public static androidx.core.view.ContentInfoCompat? performReceiveContent(android.view.View, androidx.core.view.ContentInfoCompat);
method public static void postInvalidateOnAnimation(android.view.View);
method public static void postInvalidateOnAnimation(android.view.View, int, int, int, int);
method public static void postOnAnimation(android.view.View, Runnable!);
@@ -2805,6 +2845,7 @@
method public static void setNestedScrollingEnabled(android.view.View, boolean);
method public static void setNextClusterForwardId(android.view.View, int);
method public static void setOnApplyWindowInsetsListener(android.view.View, androidx.core.view.OnApplyWindowInsetsListener?);
+ method public static void setOnReceiveContentListener(android.view.View, String![]?, androidx.core.view.OnReceiveContentListener?);
method @Deprecated public static void setOverScrollMode(android.view.View!, int);
method public static void setPaddingRelative(android.view.View, @Px int, @Px int, @Px int, @Px int);
method @Deprecated public static void setPivotX(android.view.View!, float);
@@ -3776,17 +3817,6 @@
method public static void showAsDropDown(android.widget.PopupWindow, android.view.View, int, int, int);
}
- public abstract class RichContentReceiverCompat<T extends android.view.View> {
- ctor public RichContentReceiverCompat();
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final androidx.core.view.inputmethod.InputConnectionCompat.OnCommitContentListener buildOnCommitContentListener(T);
- method public abstract java.util.Set<java.lang.String!> getSupportedMimeTypes();
- method public abstract boolean onReceive(T, android.content.ClipData, int, int);
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final void populateEditorInfoContentMimeTypes(android.view.inputmethod.InputConnection?, android.view.inputmethod.EditorInfo?);
- field public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1; // 0x1
- field public static final int SOURCE_CLIPBOARD = 0; // 0x0
- field public static final int SOURCE_INPUT_METHOD = 1; // 0x1
- }
-
@Deprecated public final class ScrollerCompat {
method @Deprecated public void abortAnimation();
method @Deprecated public boolean computeScrollOffset();
@@ -3845,10 +3875,9 @@
@IntDef({androidx.core.widget.TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE, androidx.core.widget.TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface TextViewCompat.AutoSizeTextType {
}
- public abstract class TextViewRichContentReceiverCompat extends androidx.core.widget.RichContentReceiverCompat<android.widget.TextView> {
- ctor public TextViewRichContentReceiverCompat();
- method public java.util.Set<java.lang.String!> getSupportedMimeTypes();
- method public boolean onReceive(android.widget.TextView, android.content.ClipData, int, int);
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class TextViewOnReceiveContentListener implements androidx.core.view.OnReceiveContentListener {
+ ctor public TextViewOnReceiveContentListener();
+ method public androidx.core.view.ContentInfoCompat? onReceiveContent(android.view.View, androidx.core.view.ContentInfoCompat);
}
public interface TintableCompoundButton {
diff --git a/core/core/lint-baseline.xml b/core/core/lint-baseline.xml
index 7f2ea99..8c91e30 100644
--- a/core/core/lint-baseline.xml
+++ b/core/core/lint-baseline.xml
@@ -11537,17 +11537,6 @@
<issue
id="UnsafeNewApiCall"
- message="This call is to a method from API 16, the call containing class androidx.core.widget.TextViewRichContentReceiverCompat is not annotated with @RequiresApi(x) where x is at least 16. Either annotate the containing class with at least @RequiresApi(16) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(16)."
- errorLine1=" paste = clip.getItemAt(i).coerceToStyledText(context);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/core/widget/TextViewRichContentReceiverCompat.java"
- line="82"
- column="47"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
message="This call is to a method from API 29, the call containing class androidx.core.os.TraceCompat is not annotated with @RequiresApi(x) where x is at least 29. Either annotate the containing class with at least @RequiresApi(29) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(29)."
errorLine1=" return Trace.isEnabled();"
errorLine2=" ~~~~~~~~~">
diff --git a/core/core/src/androidTest/AndroidManifest.xml b/core/core/src/androidTest/AndroidManifest.xml
index d8170ee..9c5a7f8 100644
--- a/core/core/src/androidTest/AndroidManifest.xml
+++ b/core/core/src/androidTest/AndroidManifest.xml
@@ -34,7 +34,7 @@
<activity android:name="androidx.core.widget.TextViewTestActivity"/>
- <activity android:name="androidx.core.widget.RichContentReceiverTestActivity"/>
+ <activity android:name="androidx.core.widget.ReceiveContentTestActivity"/>
<activity android:name="androidx.core.widget.TestContentViewActivity"/>
diff --git a/core/core/src/androidTest/java/androidx/core/view/ContentInfoCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/ContentInfoCompatTest.java
new file mode 100644
index 0000000..15b5fa7
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/view/ContentInfoCompatTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.core.view;
+
+import static androidx.core.view.ContentInfoCompat.SOURCE_CLIPBOARD;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ClipData;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Pair;
+
+import androidx.core.util.Predicate;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ContentInfoCompatTest {
+
+ @Test
+ public void testPartition_multipleItems() throws Exception {
+ Uri sampleUri = Uri.parse("content://com.example/path");
+ ClipData clip = ClipData.newPlainText("", "Hello");
+ clip.addItem(new ClipData.Item("Hi", "<b>Salut</b>"));
+ clip.addItem(new ClipData.Item(sampleUri));
+ ContentInfoCompat payload = new ContentInfoCompat.Builder(clip, SOURCE_CLIPBOARD)
+ .setFlags(ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT)
+ .setLinkUri(Uri.parse("http://example.com"))
+ .setExtras(new Bundle())
+ .build();
+
+ // Test splitting when some items match and some don't.
+ Pair<ContentInfoCompat, ContentInfoCompat> split;
+ split = payload.partition(new Predicate<ClipData.Item>() {
+ @Override
+ public boolean test(ClipData.Item item) {
+ return item.getUri() != null;
+ }
+ });
+ assertThat(split.first.getClip().getItemCount()).isEqualTo(1);
+ assertThat(split.second.getClip().getItemCount()).isEqualTo(2);
+ assertThat(split.first.getClip().getItemAt(0).getUri()).isEqualTo(sampleUri);
+ assertThat(split.first.getClip().getDescription()).isNotSameInstanceAs(
+ payload.getClip().getDescription());
+ assertThat(split.second.getClip().getDescription()).isNotSameInstanceAs(
+ payload.getClip().getDescription());
+ assertThat(split.first.getSource()).isEqualTo(SOURCE_CLIPBOARD);
+ assertThat(split.first.getLinkUri()).isNotNull();
+ assertThat(split.first.getExtras()).isNotNull();
+ assertThat(split.second.getSource()).isEqualTo(SOURCE_CLIPBOARD);
+ assertThat(split.second.getLinkUri()).isNotNull();
+ assertThat(split.second.getExtras()).isNotNull();
+
+ // Test splitting when none of the items match.
+ split = payload.partition(new Predicate<ClipData.Item>() {
+ @Override
+ public boolean test(ClipData.Item item) {
+ return false;
+ }
+ });
+ assertThat(split.first).isNull();
+ assertThat(split.second).isSameInstanceAs(payload);
+
+ // Test splitting when all of the items match.
+ split = payload.partition(new Predicate<ClipData.Item>() {
+ @Override
+ public boolean test(ClipData.Item item) {
+ return true;
+ }
+ });
+ assertThat(split.first).isSameInstanceAs(payload);
+ assertThat(split.second).isNull();
+ }
+
+ @Test
+ public void testPartition_singleItem() throws Exception {
+ ClipData clip = ClipData.newPlainText("", "Hello");
+ ContentInfoCompat payload = new ContentInfoCompat.Builder(clip, SOURCE_CLIPBOARD)
+ .setFlags(ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT)
+ .setLinkUri(Uri.parse("http://example.com"))
+ .setExtras(new Bundle())
+ .build();
+
+ Pair<ContentInfoCompat, ContentInfoCompat> split;
+ split = payload.partition(new Predicate<ClipData.Item>() {
+ @Override
+ public boolean test(ClipData.Item item) {
+ return false;
+ }
+ });
+ assertThat(split.first).isNull();
+ assertThat(split.second).isSameInstanceAs(payload);
+
+ split = payload.partition(new Predicate<ClipData.Item>() {
+ @Override
+ public boolean test(ClipData.Item item) {
+ return true;
+ }
+ });
+ assertThat(split.first).isSameInstanceAs(payload);
+ assertThat(split.second).isNull();
+ }
+}
diff --git a/core/core/src/androidTest/java/androidx/core/view/ViewCompatReceiveContentTest.java b/core/core/src/androidTest/java/androidx/core/view/ViewCompatReceiveContentTest.java
new file mode 100644
index 0000000..97d1d7b
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/view/ViewCompatReceiveContentTest.java
@@ -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.core.view;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import android.app.Activity;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.R;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ActivityTestRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class ViewCompatReceiveContentTest {
+ @Rule
+ public final ActivityTestRule<ViewCompatActivity> mActivityTestRule =
+ new ActivityTestRule<>(ViewCompatActivity.class);
+
+ private View mView;
+ private OnReceiveContentListener mMockReceiver;
+
+ @UiThreadTest
+ @Before
+ public void before() {
+ final Activity activity = mActivityTestRule.getActivity();
+ mView = activity.findViewById(androidx.core.test.R.id.view);
+ mMockReceiver = Mockito.mock(OnReceiveContentListener.class);
+ }
+
+ @UiThreadTest
+ @Test
+ public void testSetOnReceiveContentListener() throws Exception {
+ // Verify that by default the getters returns null.
+ assertThat(ViewCompat.getOnReceiveContentMimeTypes(mView)).isNull();
+ assertThat(getListener(mView)).isNull();
+
+ // Verify that setting non-null MIME types and a non-null receiver works.
+ String[] mimeTypes = new String[] {"image/*", "video/mp4"};
+ ViewCompat.setOnReceiveContentListener(mView, mimeTypes, mMockReceiver);
+ assertThat(ViewCompat.getOnReceiveContentMimeTypes(mView)).isSameInstanceAs(mimeTypes);
+ assertThat(getListener(mView)).isSameInstanceAs(mMockReceiver);
+
+ // Verify that setting null MIME types and a null receiver works.
+ ViewCompat.setOnReceiveContentListener(mView, null, null);
+ assertThat(ViewCompat.getOnReceiveContentMimeTypes(mView)).isNull();
+ assertThat(getListener(mView)).isNull();
+
+ // Verify that setting empty MIME types and a null receiver works.
+ ViewCompat.setOnReceiveContentListener(mView, new String[0], null);
+ assertThat(ViewCompat.getOnReceiveContentMimeTypes(mView)).isNull();
+ assertThat(getListener(mView)).isNull();
+
+ // Verify that setting MIME types with a null receiver works.
+ ViewCompat.setOnReceiveContentListener(mView, mimeTypes, null);
+ assertThat(ViewCompat.getOnReceiveContentMimeTypes(mView)).isSameInstanceAs(mimeTypes);
+ assertThat(getListener(mView)).isNull();
+
+ // Verify that setting null or empty MIME types with a non-null receiver is not allowed.
+ try {
+ ViewCompat.setOnReceiveContentListener(mView, null, mMockReceiver);
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException expected) { }
+ try {
+ ViewCompat.setOnReceiveContentListener(mView, new String[0], mMockReceiver);
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException expected) { }
+
+ // Verify that passing "*/*" as a MIME type is not allowed.
+ try {
+ ViewCompat.setOnReceiveContentListener(mView, new String[] {"image/gif", "*/*"},
+ mMockReceiver);
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException expected) { }
+ }
+
+ @Nullable
+ private static OnReceiveContentListener getListener(@NonNull View view) {
+ return (OnReceiveContentListener) view.getTag(R.id.tag_on_receive_content_listener);
+ }
+}
diff --git a/core/core/src/androidTest/java/androidx/core/view/WindowInsetsCompatActivityTest.kt b/core/core/src/androidTest/java/androidx/core/view/WindowInsetsCompatActivityTest.kt
index f100253..6331c51 100644
--- a/core/core/src/androidTest/java/androidx/core/view/WindowInsetsCompatActivityTest.kt
+++ b/core/core/src/androidTest/java/androidx/core/view/WindowInsetsCompatActivityTest.kt
@@ -16,6 +16,7 @@
package androidx.core.view
+import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Build
import android.view.View
@@ -26,6 +27,7 @@
import androidx.core.test.R
import androidx.core.view.WindowInsetsCompat.Type
import androidx.test.core.app.ActivityScenario
+import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onIdle
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
@@ -33,6 +35,7 @@
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.assertThat
import androidx.test.espresso.matcher.ViewMatchers.hasFocus
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
@@ -181,6 +184,54 @@
}
}
+ @SdkSuppress(minSdkVersion = 23, maxSdkVersion = 29)
+ @Test
+ @Ignore("IME tests are inherently flaky, but still useful for local testing.")
+ public fun ime_insets_cleared_on_back() {
+ // Test do not currently work on Cuttlefish
+ assumeNotCuttlefish()
+ assumeSoftInputMode(SOFT_INPUT_ADJUST_RESIZE)
+
+ val expectedListenerPasses = 2
+ val latch = CountDownLatch(expectedListenerPasses)
+ val received = AtomicReference<WindowInsetsCompat>()
+ val container: View = scenario.withActivity { findViewById(R.id.container) }
+
+ // Tell the window that our view will fit system windows
+ scenario.onActivity { activity ->
+ WindowCompat.setDecorFitsSystemWindows(activity.window, false)
+ }
+
+ onView(withId(R.id.edittext))
+ .perform(click())
+ .check(matches(hasFocus()))
+
+ // Set a listener to catch WindowInsets
+ ViewCompat
+ .setOnApplyWindowInsetsListener(container.rootView) { _, insets: WindowInsetsCompat ->
+ received.set(insets)
+ latch.countDown()
+ WindowInsetsCompat.CONSUMED
+ }
+
+ scenario.onActivity { activity ->
+ activity.startActivity(Intent(activity, activity::class.java))
+ }
+
+ Espresso.pressBackUnconditionally()
+ onView(withId(R.id.edittext))
+ .check(matches(isDisplayed()))
+ assertThat(
+ "OnApplyWindowListener should have been called $expectedListenerPasses times but was " +
+ "called ${expectedListenerPasses - latch.count} times",
+ latch.await(2, TimeUnit.SECONDS), `is`(true)
+ )
+
+ // Check that the IME insets is equal to 0
+ val insets = received.get()
+ assertEquals(0, insets.getInsets(Type.ime()).bottom)
+ }
+
@SdkSuppress(minSdkVersion = 23)
@Test
@Ignore("IME tests are inherently flaky, but still useful for local testing.")
diff --git a/core/core/src/androidTest/java/androidx/core/view/WindowInsetsCompatTest.kt b/core/core/src/androidTest/java/androidx/core/view/WindowInsetsCompatTest.kt
index 8c4eb55..e17940d 100644
--- a/core/core/src/androidTest/java/androidx/core/view/WindowInsetsCompatTest.kt
+++ b/core/core/src/androidTest/java/androidx/core/view/WindowInsetsCompatTest.kt
@@ -22,6 +22,7 @@
import androidx.test.filters.SmallTest
import org.hamcrest.Matchers.notNullValue
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertThat
import org.junit.Assert.assertTrue
@@ -218,19 +219,37 @@
}
@Test
- fun test_equals() {
+ public fun test_equals() {
val result = WindowInsetsCompat.Builder()
.setInsets(Type.systemBars(), Insets.of(1, 2, 3, 4))
.setInsetsIgnoringVisibility(Type.systemBars(), Insets.of(11, 12, 13, 14))
.build()
+ result.setRootViewData(Insets.of(0, 0, 0, 15))
val result2 = WindowInsetsCompat.Builder()
.setInsets(Type.systemBars(), Insets.of(1, 2, 3, 4))
.setInsetsIgnoringVisibility(Type.systemBars(), Insets.of(11, 12, 13, 14))
.build()
+ result2.setRootViewData(Insets.of(0, 0, 0, 15))
assertEquals(result, result2)
}
@Test
+ @SdkSuppress(minSdkVersion = 20)
+ public fun test_not_equals_root_visible_insets() {
+ val result = WindowInsetsCompat.Builder()
+ .setInsets(Type.systemBars(), Insets.of(1, 2, 3, 4))
+ .setInsetsIgnoringVisibility(Type.systemBars(), Insets.of(11, 12, 13, 14))
+ .build()
+ result.setRootViewData(Insets.of(0, 0, 0, 15))
+ val result2 = WindowInsetsCompat.Builder()
+ .setInsets(Type.systemBars(), Insets.of(1, 2, 3, 4))
+ .setInsetsIgnoringVisibility(Type.systemBars(), Insets.of(11, 12, 13, 14))
+ .build()
+ result2.setRootViewData(Insets.of(0, 0, 0, 16))
+ assertNotEquals(result, result2)
+ }
+
+ @Test
fun test_hashCode() {
val result = WindowInsetsCompat.Builder()
.setInsets(Type.systemBars(), Insets.of(1, 2, 3, 4))
diff --git a/core/core/src/androidTest/java/androidx/core/widget/RichContentReceiverTestActivity.java b/core/core/src/androidTest/java/androidx/core/widget/ReceiveContentTestActivity.java
similarity index 85%
rename from core/core/src/androidTest/java/androidx/core/widget/RichContentReceiverTestActivity.java
rename to core/core/src/androidTest/java/androidx/core/widget/ReceiveContentTestActivity.java
index 83a6ba0..bc5a607 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/RichContentReceiverTestActivity.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/ReceiveContentTestActivity.java
@@ -20,9 +20,9 @@
import androidx.core.test.R;
-public class RichContentReceiverTestActivity extends BaseTestActivity {
+public class ReceiveContentTestActivity extends BaseTestActivity {
@Override
protected int getContentViewLayoutResId() {
- return R.layout.rich_content_receiver_activity;
+ return R.layout.receive_content_activity;
}
}
diff --git a/core/core/src/androidTest/java/androidx/core/widget/RichContentReceiverCompatTest.java b/core/core/src/androidTest/java/androidx/core/widget/TextViewOnReceiveContentListenerTest.java
similarity index 73%
rename from core/core/src/androidTest/java/androidx/core/widget/RichContentReceiverCompatTest.java
rename to core/core/src/androidTest/java/androidx/core/widget/TextViewOnReceiveContentListenerTest.java
index 6310271..a8760f0 100644
--- a/core/core/src/androidTest/java/androidx/core/widget/RichContentReceiverCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/widget/TextViewOnReceiveContentListenerTest.java
@@ -16,28 +16,25 @@
package androidx.core.widget;
-import static androidx.core.widget.RichContentReceiverCompat.FLAG_CONVERT_TO_PLAIN_TEXT;
-import static androidx.core.widget.RichContentReceiverCompat.SOURCE_CLIPBOARD;
-import static androidx.core.widget.RichContentReceiverCompat.SOURCE_INPUT_METHOD;
+import static androidx.core.view.ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT;
+import static androidx.core.view.ContentInfoCompat.SOURCE_CLIPBOARD;
+import static androidx.core.view.ContentInfoCompat.SOURCE_INPUT_METHOD;
import static com.google.common.truth.Truth.assertThat;
-import static java.util.Collections.singleton;
-
import android.content.ClipData;
import android.net.Uri;
import android.text.Selection;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.UnderlineSpan;
-import android.view.inputmethod.BaseInputConnection;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
import android.widget.EditText;
-import android.widget.TextView;
import androidx.core.test.R;
-import androidx.core.view.inputmethod.EditorInfoCompat;
+import androidx.core.view.ContentInfoCompat;
+import androidx.core.view.ContentInfoCompat.Flags;
+import androidx.core.view.ContentInfoCompat.Source;
+import androidx.core.view.OnReceiveContentListener;
import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
@@ -51,26 +48,20 @@
@MediumTest
@RunWith(AndroidJUnit4.class)
-public class RichContentReceiverCompatTest {
+public class TextViewOnReceiveContentListenerTest {
@Rule
- public final ActivityTestRule<RichContentReceiverTestActivity> mActivityTestRule =
- new ActivityTestRule<>(RichContentReceiverTestActivity.class);
+ public final ActivityTestRule<ReceiveContentTestActivity> mActivityTestRule =
+ new ActivityTestRule<>(ReceiveContentTestActivity.class);
private EditText mEditText;
- private RichContentReceiverCompat<TextView> mReceiver;
+ private TextViewOnReceiveContentListener mReceiver;
@Before
public void before() {
- RichContentReceiverTestActivity activity = mActivityTestRule.getActivity();
- mEditText = activity.findViewById(R.id.edit_text_for_rich_content_receiver);
- mReceiver = new TextViewRichContentReceiverCompat() {};
- }
-
- @UiThreadTest
- @Test
- public void testGetSupportedMimeTypes() throws Exception {
- assertThat(mReceiver.getSupportedMimeTypes()).isEqualTo(singleton("text/*"));
+ ReceiveContentTestActivity activity = mActivityTestRule.getActivity();
+ mEditText = activity.findViewById(R.id.edit_text);
+ mReceiver = new TextViewOnReceiveContentListener();
}
@UiThreadTest
@@ -247,44 +238,12 @@
assertTextAndCursorPosition("xz", 1);
}
- @UiThreadTest
- @Test
- public void testPopulateEditorInfoContentMimeTypes() throws Exception {
- InputConnection ic = new BaseInputConnection(mEditText, true);
- EditorInfo outAttrs = new EditorInfo();
-
- // The field contentMimeTypes in outAttrs should be set to the MIME types of the receiver.
- mReceiver.populateEditorInfoContentMimeTypes(ic, outAttrs);
- assertThat(EditorInfoCompat.getContentMimeTypes(outAttrs)).isEqualTo(
- new String[] {"text/*"});
-
- // If the field contentMimeTypes in outAttrs already has a value assigned, it should be
- // overwritten with the MIME types of the receiver.
- EditorInfoCompat.setContentMimeTypes(outAttrs, new String[] {"video/mp4"});
- mReceiver.populateEditorInfoContentMimeTypes(ic, outAttrs);
- assertThat(EditorInfoCompat.getContentMimeTypes(outAttrs)).isEqualTo(
- new String[] {"text/*"});
- }
-
- @UiThreadTest
- @Test
- public void testPopulateEditorInfoContentMimeTypes_nulls() throws Exception {
- InputConnection ic = new BaseInputConnection(mEditText, true);
- EditorInfo outAttrs = new EditorInfo();
-
- // If the ic arg is null, outAttrs should not be populated.
- mReceiver.populateEditorInfoContentMimeTypes(null, outAttrs);
- assertThat(EditorInfoCompat.getContentMimeTypes(outAttrs)).isEqualTo(new String[0]);
-
- // If the outAttrs arg is null, it should not be populated.
- mReceiver.populateEditorInfoContentMimeTypes(ic, null);
- assertThat(EditorInfoCompat.getContentMimeTypes(outAttrs)).isEqualTo(new String[0]);
- }
-
- private boolean onReceive(final RichContentReceiverCompat<TextView> receiver,
- final ClipData clip, @RichContentReceiverCompat.Source final int source,
- final int flags) {
- return receiver.onReceive(mEditText, clip, source, flags);
+ private boolean onReceive(final OnReceiveContentListener receiver, ClipData clip,
+ @Source int source, @Flags int flags) {
+ ContentInfoCompat payload = new ContentInfoCompat.Builder(clip, source)
+ .setFlags(flags)
+ .build();
+ return receiver.onReceiveContent(mEditText, payload) == null;
}
private void setTextAndCursor(final String text, final int cursorPosition) {
diff --git a/core/core/src/androidTest/res/layout/rich_content_receiver_activity.xml b/core/core/src/androidTest/res/layout/receive_content_activity.xml
similarity index 93%
rename from core/core/src/androidTest/res/layout/rich_content_receiver_activity.xml
rename to core/core/src/androidTest/res/layout/receive_content_activity.xml
index 81b6479..a4ea03c 100644
--- a/core/core/src/androidTest/res/layout/rich_content_receiver_activity.xml
+++ b/core/core/src/androidTest/res/layout/receive_content_activity.xml
@@ -21,7 +21,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
- android:id="@+id/edit_text_for_rich_content_receiver"
+ android:id="@+id/edit_text"
android:layout_width="200dip"
android:layout_height="60dip" />
</FrameLayout>
diff --git a/core/core/src/main/java/androidx/core/app/ShareCompat.java b/core/core/src/main/java/androidx/core/app/ShareCompat.java
index 79bce4d8..8d6b8ac 100644
--- a/core/core/src/main/java/androidx/core/app/ShareCompat.java
+++ b/core/core/src/main/java/androidx/core/app/ShareCompat.java
@@ -230,7 +230,10 @@
*
* @param item MenuItem to configure for sharing
* @param shareIntent IntentBuilder with data about the content to share
+ *
+ * @deprecated Use the system sharesheet. See https://developer.android.com/training/sharing/send
*/
+ @Deprecated
public static void configureMenuItem(@NonNull MenuItem item,
@NonNull IntentBuilder shareIntent) {
ActionProvider itemProvider = item.getActionProvider();
@@ -259,7 +262,10 @@
* @param menuItemId ID of the share item within menu
* @param shareIntent IntentBuilder with data about the content to share
* @see #configureMenuItem(MenuItem, IntentBuilder)
+ *
+ * @deprecated Use the system sharesheet. See https://developer.android.com/training/sharing/send
*/
+ @Deprecated
public static void configureMenuItem(@NonNull Menu menu, @IdRes int menuItemId,
@NonNull IntentBuilder shareIntent) {
MenuItem item = menu.findItem(menuItemId);
@@ -423,12 +429,6 @@
/**
* Start a chooser activity for the current share intent.
- *
- * <p>Note that under most circumstances you should use
- * {@link ShareCompat#configureMenuItem(MenuItem, IntentBuilder)
- * ShareCompat.configureMenuItem()} to add a Share item to the menu while
- * presenting a detail view of the content to be shared instead
- * of invoking this directly.</p>
*/
public void startChooser() {
mContext.startActivity(createChooserIntent());
diff --git a/core/core/src/main/java/androidx/core/os/BuildCompat.java b/core/core/src/main/java/androidx/core/os/BuildCompat.java
index e14d683..ba91322 100644
--- a/core/core/src/main/java/androidx/core/os/BuildCompat.java
+++ b/core/core/src/main/java/androidx/core/os/BuildCompat.java
@@ -114,18 +114,17 @@
}
/**
- * Checks if the device is running on a pre-release version of Android R or a release
- * version of Android R or newer.
+ * Checks if the device is running on release version of Android R or newer.
* <p>
- * <strong>Note:</strong> When Android R is finalized for release, this method will be
- * deprecated and all calls should be replaced with
- * {@code Build.VERSION.SDK_INT >= Build.VERSION_CODES.R}.
- *
* @return {@code true} if R APIs are available for use, {@code false} otherwise
+ * @deprecated Android R is a finalized release and this method is no longer necessary. It
+ * will be removed in a future release of the Support Library. Instead, use
+ * {@code Build.VERSION.SDK_INT >= Build.VERSION_CODES.R}.
*/
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R)
+ @Deprecated
public static boolean isAtLeastR() {
- return VERSION.SDK_INT >= 30 || VERSION.CODENAME.equals("R");
+ return VERSION.SDK_INT >= 30;
}
/**
diff --git a/core/core/src/main/java/androidx/core/view/ContentInfoCompat.java b/core/core/src/main/java/androidx/core/view/ContentInfoCompat.java
new file mode 100644
index 0000000..afa48083
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/view/ContentInfoCompat.java
@@ -0,0 +1,364 @@
+/*
+ * 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.core.view;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Pair;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Holds all the relevant data for a request to {@link OnReceiveContentListener}.
+ */
+// This class has the "Compat" suffix because it will integrate with (ie, wrap) the SDK API once
+// that is available.
+public final class ContentInfoCompat {
+
+ /**
+ * Specifies the UI through which content is being inserted. Future versions of Android may
+ * support additional values.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(value = {SOURCE_APP, SOURCE_CLIPBOARD, SOURCE_INPUT_METHOD})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Source {
+ }
+
+ /**
+ * Specifies that the operation was triggered by the app that contains the target view.
+ */
+ public static final int SOURCE_APP = 0;
+
+ /**
+ * Specifies that the operation was triggered by a paste from the clipboard (e.g. "Paste" or
+ * "Paste as plain text" action in the insertion/selection menu).
+ */
+ public static final int SOURCE_CLIPBOARD = 1;
+
+ /**
+ * Specifies that the operation was triggered from the soft keyboard (also known as input
+ * method editor or IME). See https://developer.android.com/guide/topics/text/image-keyboard
+ * for more info.
+ */
+ public static final int SOURCE_INPUT_METHOD = 2;
+
+ /**
+ * Returns the symbolic name of the given source.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @NonNull
+ static String sourceToString(@Source int source) {
+ switch (source) {
+ case SOURCE_APP:
+ return "SOURCE_APP";
+ case SOURCE_CLIPBOARD:
+ return "SOURCE_CLIPBOARD";
+ case SOURCE_INPUT_METHOD:
+ return "SOURCE_INPUT_METHOD";
+ }
+ return String.valueOf(source);
+ }
+
+ /**
+ * Flags to configure the insertion behavior.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @IntDef(flag = true, value = {FLAG_CONVERT_TO_PLAIN_TEXT})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Flags {
+ }
+
+ /**
+ * Flag requesting that the content should be converted to plain text prior to inserting.
+ */
+ public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1 << 0;
+
+ /**
+ * Returns the symbolic names of the set flags or {@code "0"} if no flags are set.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @NonNull
+ static String flagsToString(@Flags int flags) {
+ if ((flags & FLAG_CONVERT_TO_PLAIN_TEXT) != 0) {
+ return "FLAG_CONVERT_TO_PLAIN_TEXT";
+ }
+ return String.valueOf(flags);
+ }
+
+ @NonNull
+ final ClipData mClip;
+ @Source
+ final int mSource;
+ @Flags
+ final int mFlags;
+ @Nullable
+ final Uri mLinkUri;
+ @Nullable
+ final Bundle mExtras;
+
+ ContentInfoCompat(Builder b) {
+ this.mClip = Preconditions.checkNotNull(b.mClip);
+ this.mSource = Preconditions.checkArgumentInRange(b.mSource, 0, SOURCE_INPUT_METHOD,
+ "source");
+ this.mFlags = Preconditions.checkFlagsArgument(b.mFlags, FLAG_CONVERT_TO_PLAIN_TEXT);
+ this.mLinkUri = b.mLinkUri;
+ this.mExtras = b.mExtras;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "ContentInfoCompat{"
+ + "clip=" + mClip
+ + ", source=" + sourceToString(mSource)
+ + ", flags=" + flagsToString(mFlags)
+ + ", linkUri=" + mLinkUri
+ + ", extras=" + mExtras
+ + "}";
+ }
+
+ /**
+ * The data to be inserted.
+ */
+ @NonNull
+ public ClipData getClip() {
+ return mClip;
+ }
+
+ /**
+ * The source of the operation. See {@code SOURCE_} constants. Future versions of Android
+ * may pass additional values.
+ */
+ @Source
+ public int getSource() {
+ return mSource;
+ }
+
+ /**
+ * Optional flags that control the insertion behavior. See {@code FLAG_} constants.
+ */
+ @Flags
+ public int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Optional http/https URI for the content that may be provided by the IME. This is only
+ * populated if the source is {@link #SOURCE_INPUT_METHOD} and if a non-empty
+ * {@link android.view.inputmethod.InputContentInfo#getLinkUri linkUri} was passed by the
+ * IME.
+ */
+ @Nullable
+ public Uri getLinkUri() {
+ return mLinkUri;
+ }
+
+ /**
+ * Optional additional metadata. If the source is {@link #SOURCE_INPUT_METHOD}, this will
+ * include the {@link android.view.inputmethod.InputConnection#commitContent opts} passed by
+ * the IME.
+ */
+ @Nullable
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Partitions this content based on the given predicate.
+ *
+ * <p>This function classifies the content and organizes it into a pair, grouping the items
+ * that matched vs didn't match the predicate.
+ *
+ * <p>Except for the {@link ClipData} items, the returned objects will contain all the same
+ * metadata as this {@link ContentInfoCompat}.
+ *
+ * @param itemPredicate The predicate to test each {@link ClipData.Item} to determine which
+ * partition to place it into.
+ * @return A pair containing the partitioned content. The pair's first object will have the
+ * content that matched the predicate, or null if none of the items matched. The pair's
+ * second object will have the content that didn't match the predicate, or null if all of
+ * the items matched.
+ */
+ @NonNull
+ public Pair<ContentInfoCompat, ContentInfoCompat> partition(
+ @NonNull androidx.core.util.Predicate<ClipData.Item> itemPredicate) {
+ if (mClip.getItemCount() == 1) {
+ boolean matched = itemPredicate.test(mClip.getItemAt(0));
+ return Pair.create(matched ? this : null, matched ? null : this);
+ }
+ ArrayList<ClipData.Item> acceptedItems = new ArrayList<>();
+ ArrayList<ClipData.Item> remainingItems = new ArrayList<>();
+ for (int i = 0; i < mClip.getItemCount(); i++) {
+ ClipData.Item item = mClip.getItemAt(i);
+ if (itemPredicate.test(item)) {
+ acceptedItems.add(item);
+ } else {
+ remainingItems.add(item);
+ }
+ }
+ if (acceptedItems.isEmpty()) {
+ return Pair.create(null, this);
+ }
+ if (remainingItems.isEmpty()) {
+ return Pair.create(this, null);
+ }
+ ContentInfoCompat accepted = new Builder(this)
+ .setClip(buildClipData(mClip.getDescription(), acceptedItems))
+ .build();
+ ContentInfoCompat remaining = new Builder(this)
+ .setClip(buildClipData(mClip.getDescription(), remainingItems))
+ .build();
+ return Pair.create(accepted, remaining);
+ }
+
+ private static ClipData buildClipData(ClipDescription description,
+ List<ClipData.Item> items) {
+ ClipData clip = new ClipData(new ClipDescription(description), items.get(0));
+ for (int i = 1; i < items.size(); i++) {
+ clip.addItem(items.get(i));
+ }
+ return clip;
+ }
+
+ /**
+ * Builder for {@link ContentInfoCompat}.
+ */
+ public static final class Builder {
+ @NonNull
+ ClipData mClip;
+ @Source
+ int mSource;
+ @Flags
+ int mFlags;
+ @Nullable
+ Uri mLinkUri;
+ @Nullable
+ Bundle mExtras;
+
+ /**
+ * Creates a new builder initialized with the data from the given builder.
+ */
+ public Builder(@NonNull ContentInfoCompat other) {
+ mClip = other.mClip;
+ mSource = other.mSource;
+ mFlags = other.mFlags;
+ mLinkUri = other.mLinkUri;
+ mExtras = other.mExtras;
+ }
+
+ /**
+ * Creates a new builder.
+ *
+ * @param clip The data to insert.
+ * @param source The source of the operation. See {@code SOURCE_} constants.
+ */
+ public Builder(@NonNull ClipData clip, @Source int source) {
+ mClip = clip;
+ mSource = source;
+ }
+
+ /**
+ * Sets the data to be inserted.
+ *
+ * @param clip The data to insert.
+ * @return this builder
+ */
+ @NonNull
+ public Builder setClip(@NonNull ClipData clip) {
+ mClip = clip;
+ return this;
+ }
+
+ /**
+ * Sets the source of the operation.
+ *
+ * @param source The source of the operation. See {@code SOURCE_} constants.
+ * @return this builder
+ */
+ @NonNull
+ public Builder setSource(@Source int source) {
+ mSource = source;
+ return this;
+ }
+
+ /**
+ * Sets flags that control content insertion behavior.
+ *
+ * @param flags Optional flags to configure the insertion behavior. Use 0 for default
+ * behavior. See {@code FLAG_} constants.
+ * @return this builder
+ */
+ @NonNull
+ public Builder setFlags(@Flags int flags) {
+ mFlags = flags;
+ return this;
+ }
+
+ /**
+ * Sets the http/https URI for the content. See
+ * {@link android.view.inputmethod.InputContentInfo#getLinkUri} for more info.
+ *
+ * @param linkUri Optional http/https URI for the content.
+ * @return this builder
+ */
+ @NonNull
+ public Builder setLinkUri(@Nullable Uri linkUri) {
+ mLinkUri = linkUri;
+ return this;
+ }
+
+ /**
+ * Sets additional metadata.
+ *
+ * @param extras Optional bundle with additional metadata.
+ * @return this builder
+ */
+ @NonNull
+ public Builder setExtras(@Nullable Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * @return A new {@link ContentInfoCompat} instance with the data from this builder.
+ */
+ @NonNull
+ public ContentInfoCompat build() {
+ return new ContentInfoCompat(this);
+ }
+ }
+}
diff --git a/core/core/src/main/java/androidx/core/view/OnReceiveContentListener.java b/core/core/src/main/java/androidx/core/view/OnReceiveContentListener.java
new file mode 100644
index 0000000..2c42b9d
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/view/OnReceiveContentListener.java
@@ -0,0 +1,99 @@
+/*
+ * 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.core.view;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Listener for apps to implement handling for insertion of content. Content may be both text and
+ * non-text (plain/styled text, HTML, images, videos, audio files, etc).
+ *
+ * <p>This listener can be attached to different types of UI components using
+ * {@link ViewCompat#setOnReceiveContentListener}.
+ *
+ * <p>Here is a sample implementation that handles content URIs and delegates the processing for
+ * text and everything else to the platform:<br>
+ * <pre class="prettyprint">
+ * // (1) Define the listener
+ * public class MyReceiver implements OnReceiveContentListener {
+ * public static final String[] MIME_TYPES = new String[] {"image/*", "video/*"};
+ *
+ * @Override
+ * public ContentInfoCompat onReceiveContent(View view, ContentInfoCompat contentInfo) {
+ * Pair<ContentInfoCompat, ContentInfoCompat> split = contentInfo.partition(
+ * item -> item.getUri() != null);
+ * ContentInfo uriContent = split.first;
+ * ContentInfo remaining = split.second;
+ * if (uriContent != null) {
+ * ClipData clip = uriContent.getClip();
+ * for (int i = 0; i < clip.getItemCount(); i++) {
+ * Uri uri = clip.getItemAt(i).getUri();
+ * // ... app-specific logic to handle the URI ...
+ * }
+ * }
+ * // Return anything that we didn't handle ourselves. This preserves the default platform
+ * // behavior for text and anything else for which we are not implementing custom handling.
+ * return remaining;
+ * }
+ * }
+ *
+ * // (2) Register the listener
+ * public class MyActivity extends Activity {
+ * @Override
+ * public void onCreate(Bundle savedInstanceState) {
+ * // ...
+ *
+ * AppCompatEditText myInput = findViewById(R.id.my_input);
+ * ViewCompat.setOnReceiveContentListener(myInput, MyReceiver.MIME_TYPES, new MyReceiver());
+ * }
+ * </pre>
+ */
+public interface OnReceiveContentListener {
+ /**
+ * Receive the given content.
+ *
+ * <p>Implementations should handle any content items of interest and return all unhandled
+ * items to preserve the default platform behavior for content that does not have app-specific
+ * handling. For example, an implementation may provide handling for content URIs (to provide
+ * support for inserting images, etc) and delegate the processing of text to the platform to
+ * preserve the common behavior for inserting text. See the class javadoc for a sample
+ * implementation and see {@link ContentInfoCompat#partition} for a convenient way to split the
+ * passed-in content.
+ *
+ * <p>If implementing handling for text: if the view has a selection, the selection should
+ * be overwritten by the passed-in content; if there's no selection, the passed-in content
+ * should be inserted at the current cursor position.
+ *
+ * <p>If implementing handling for non-text content (e.g. images): the content may be
+ * inserted inline, or it may be added as an attachment (could potentially be shown in a
+ * completely separate view).
+ *
+ * @param view The view where the content insertion was requested.
+ * @param payload The content to insert and related metadata.
+ *
+ * @return The portion of the passed-in content whose processing should be delegated to
+ * the platform. Return null if all content was handled in some way. Actual insertion of
+ * the content may be processed asynchronously in the background and may or may not
+ * succeed even if this method returns null. For example, an app may end up not inserting
+ * an item if it exceeds the app's size limit for that type of content.
+ */
+ @Nullable
+ ContentInfoCompat onReceiveContent(@NonNull View view, @NonNull ContentInfoCompat payload);
+}
diff --git a/core/core/src/main/java/androidx/core/view/OnReceiveContentViewBehavior.java b/core/core/src/main/java/androidx/core/view/OnReceiveContentViewBehavior.java
new file mode 100644
index 0000000..9b67b78
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/view/OnReceiveContentViewBehavior.java
@@ -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.core.view;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Interface for widgets to implement default behavior for receiving content. Content may be both
+ * text and non-text (plain/styled text, HTML, images, videos, audio files, etc).
+ *
+ * <p>Widgets should implement this interface to define the default behavior for receiving content.
+ * Apps wishing to provide custom behavior for receiving content should set a listener via
+ * {@link ViewCompat#setOnReceiveContentListener}. See {@link ViewCompat#performReceiveContent} for
+ * more info.
+ */
+public interface OnReceiveContentViewBehavior {
+ /**
+ * Implements a view's default behavior for receiving content.
+ *
+ * @param payload The content to insert and related metadata.
+ *
+ * @return The portion of the passed-in content that was not handled (may be all, some, or none
+ * of the passed-in content).
+ */
+ @Nullable
+ ContentInfoCompat onReceiveContent(@NonNull ContentInfoCompat payload);
+}
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 c5c8c38..14ac552 100644
--- a/core/core/src/main/java/androidx/core/view/ViewCompat.java
+++ b/core/core/src/main/java/androidx/core/view/ViewCompat.java
@@ -22,6 +22,7 @@
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ClipData;
+import android.content.ClipDescription;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
@@ -53,6 +54,7 @@
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeProvider;
+import android.view.inputmethod.InputConnection;
import androidx.annotation.FloatRange;
import androidx.annotation.IdRes;
@@ -65,6 +67,7 @@
import androidx.annotation.UiThread;
import androidx.collection.SimpleArrayMap;
import androidx.core.R;
+import androidx.core.util.Preconditions;
import androidx.core.view.AccessibilityDelegateCompat.AccessibilityDelegateAdapter;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
@@ -78,6 +81,7 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -2509,12 +2513,36 @@
}
v.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
+ WindowInsetsCompat mLastInsets = null;
+ WindowInsets mReturnedInsets = null;
+
@Override
- public WindowInsets onApplyWindowInsets(View view, WindowInsets insets) {
+ public WindowInsets onApplyWindowInsets(final View view,
+ final WindowInsets insets) {
WindowInsetsCompat compatInsets = WindowInsetsCompat
.toWindowInsetsCompat(insets, view);
+ if (Build.VERSION.SDK_INT < 30) {
+ if (compatInsets.equals(mLastInsets)) {
+ // We got the same insets we just return the previously computed insets.
+ return mReturnedInsets;
+ }
+ mLastInsets = compatInsets;
+ }
compatInsets = listener.onApplyWindowInsets(view, compatInsets);
- return compatInsets.toWindowInsets();
+
+ if (Build.VERSION.SDK_INT >= 30) {
+ return compatInsets.toWindowInsets();
+ }
+
+ // On API < 30, the visibleInsets, used to built WindowInsetsCompat, are
+ // updated after the insets dispatch so we don't have the updated visible
+ // insets at that point. As a workaround, we req-apply the insets so we know
+ // that we'll have the right value the next time it's called.
+ requestApplyInsets(view);
+ // Keep a copy in case the insets haven't changed on the next call so we don't
+ // need to call the listener again.
+ mReturnedInsets = compatInsets.toWindowInsets();
+ return mReturnedInsets;
}
});
}
@@ -2668,6 +2696,129 @@
}
/**
+ * Sets the listener to be used to handle insertion of content into the given view.
+ *
+ * <p>Depending on the type of view, this listener may be invoked for different scenarios. For
+ * example, for an AppCompatEditText, this listener will be invoked for the following 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})
+ * </ol>
+ *
+ * <p>When setting a listener, clients should also declare the MIME types accepted by it.
+ * When invoked with other types of content, the listener may reject the content (defer to
+ * the default platform behavior) or execute some other fallback logic. The MIME types
+ * declared here allow different features to optionally alter their behavior. For example,
+ * the soft keyboard may choose to hide its UI for inserting GIFs for a particular input
+ * field if the MIME types set here for that field don't include "image/gif" or "image/*".
+ *
+ * <p>Note: MIME type matching in the Android framework is case-sensitive, unlike formal RFC
+ * MIME types. As a result, you should always write your MIME types with lowercase letters,
+ * or use {@link android.content.Intent#normalizeMimeType} to ensure that it is converted to
+ * lowercase.
+ *
+ * @param view The target view.
+ * @param mimeTypes The MIME types accepted by the given listener. These may use patterns
+ * such as "image/*", but may not start with a wildcard. This argument must
+ * not be null or empty if a non-null listener is passed in.
+ * @param listener The listener to use. This can be null to reset to the default behavior.
+ */
+ public static void setOnReceiveContentListener(@NonNull View view, @Nullable String[] mimeTypes,
+ @Nullable OnReceiveContentListener listener) {
+ mimeTypes = (mimeTypes == null || mimeTypes.length == 0) ? null : mimeTypes;
+ if (listener != null) {
+ Preconditions.checkArgument(mimeTypes != null,
+ "When the listener is set, MIME types must also be set");
+ }
+ if (mimeTypes != null) {
+ boolean hasLeadingWildcard = false;
+ for (String mimeType : mimeTypes) {
+ if (mimeType.startsWith("*")) {
+ hasLeadingWildcard = true;
+ break;
+ }
+ }
+ Preconditions.checkArgument(!hasLeadingWildcard,
+ "A MIME type set here must not start with *: " + Arrays.toString(mimeTypes));
+ }
+ view.setTag(R.id.tag_on_receive_content_mime_types, mimeTypes);
+ view.setTag(R.id.tag_on_receive_content_listener, listener);
+ }
+
+ /**
+ * Returns the MIME types accepted by the listener configured on the given view via
+ * {@link #setOnReceiveContentListener}. By default returns null.
+ *
+ * <p>Different features (e.g. pasting from the clipboard, inserting stickers from the soft
+ * keyboard, etc) may optionally use this metadata to conditionally alter their behavior. For
+ * example, a soft keyboard may choose to hide its UI for inserting GIFs for a particular
+ * input field if the MIME types returned here for that field don't include "image/gif" or
+ * "image/*".
+ *
+ * <p>Note: Comparisons of MIME types should be performed using utilities such as
+ * {@link ClipDescription#compareMimeTypes} rather than simple string equality, in order to
+ * correctly handle patterns such as "text/*", "image/*", etc. Note that MIME type matching
+ * in the Android framework is case-sensitive, unlike formal RFC MIME types. As a result,
+ * you should always write your MIME types with lowercase letters, or use
+ * {@link android.content.Intent#normalizeMimeType} to ensure that it is converted to
+ * lowercase.
+ *
+ * @param view The target view.
+ *
+ * @return The MIME types accepted by the {@link OnReceiveContentListener} for the given view
+ * (may include patterns such as "image/*").
+ */
+ @Nullable
+ public static String[] getOnReceiveContentMimeTypes(@NonNull View view) {
+ return (String[]) view.getTag(R.id.tag_on_receive_content_mime_types);
+ }
+
+ /**
+ * Receives the given content.
+ *
+ * <p>If a listener is set, invokes the listener. If the listener returns a non-null result,
+ * executes the fallback handling for the portion of the content returned by the listener.
+ *
+ * <p>If no listener is set, executes the fallback handling.
+ *
+ * <p>The fallback handling is defined by the target view if the view implements
+ * {@link OnReceiveContentViewBehavior}, or is simply a no-op.
+ *
+ * @param view The target view.
+ * @param payload The content to insert and related metadata.
+ *
+ * @return The portion of the passed-in content that was not handled (may be all, some, or none
+ * of the passed-in content).
+ */
+ @Nullable
+ public static ContentInfoCompat performReceiveContent(@NonNull View view,
+ @NonNull ContentInfoCompat payload) {
+ OnReceiveContentListener listener =
+ (OnReceiveContentListener) view.getTag(R.id.tag_on_receive_content_listener);
+ if (listener != null) {
+ ContentInfoCompat remaining = listener.onReceiveContent(view, payload);
+ return (remaining == null) ? null : getFallback(view).onReceiveContent(remaining);
+ }
+ return getFallback(view).onReceiveContent(payload);
+ }
+
+ private static OnReceiveContentViewBehavior getFallback(@NonNull View view) {
+ if (view instanceof OnReceiveContentViewBehavior) {
+ return ((OnReceiveContentViewBehavior) view);
+ }
+ return NO_OP_ON_RECEIVE_CONTENT_VIEW_BEHAVIOR;
+ }
+
+ private static final OnReceiveContentViewBehavior NO_OP_ON_RECEIVE_CONTENT_VIEW_BEHAVIOR =
+ new OnReceiveContentViewBehavior() {
+ @Override
+ public ContentInfoCompat onReceiveContent(@NonNull ContentInfoCompat payload) {
+ return payload;
+ }
+ };
+
+ /**
* Controls whether the entire hierarchy under this view will save its
* state when a state saving traversal occurs from its parent.
*
diff --git a/core/core/src/main/java/androidx/core/view/WindowInsetsCompat.java b/core/core/src/main/java/androidx/core/view/WindowInsetsCompat.java
index 6e674e7..31a8a5b 100644
--- a/core/core/src/main/java/androidx/core/view/WindowInsetsCompat.java
+++ b/core/core/src/main/java/androidx/core/view/WindowInsetsCompat.java
@@ -884,7 +884,7 @@
private Insets mSystemWindowInsets = null;
private WindowInsetsCompat mRootWindowInsets;
- private Insets mRootViewVisibleInsets;
+ Insets mRootViewVisibleInsets;
Impl20(@NonNull WindowInsetsCompat host, @NonNull WindowInsets insets) {
super(host);
@@ -1168,6 +1168,13 @@
private static void logReflectionError(Exception e) {
Log.e(TAG, "Failed to get visible insets. (Reflection error). " + e.getMessage(), e);
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (!super.equals(o)) return false;
+ Impl20 impl20 = (Impl20) o;
+ return Objects.equals(mRootViewVisibleInsets, impl20.mRootViewVisibleInsets);
+ }
}
@RequiresApi(21)
@@ -1241,7 +1248,8 @@
if (!(o instanceof Impl28)) return false;
Impl28 otherImpl28 = (Impl28) o;
// On API 28+ we can rely on WindowInsets.equals()
- return Objects.equals(mPlatformInsets, otherImpl28.mPlatformInsets);
+ return Objects.equals(mPlatformInsets, otherImpl28.mPlatformInsets)
+ && Objects.equals(mRootViewVisibleInsets, otherImpl28.mRootViewVisibleInsets);
}
@Override
diff --git a/core/core/src/main/java/androidx/core/widget/RichContentReceiverCompat.java b/core/core/src/main/java/androidx/core/widget/RichContentReceiverCompat.java
deleted file mode 100644
index 2a66954..0000000
--- a/core/core/src/main/java/androidx/core/widget/RichContentReceiverCompat.java
+++ /dev/null
@@ -1,223 +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.core.widget;
-
-import android.content.ClipData;
-import android.content.ClipDescription;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.View;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.core.view.inputmethod.EditorInfoCompat;
-import androidx.core.view.inputmethod.InputConnectionCompat;
-import androidx.core.view.inputmethod.InputContentInfoCompat;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.Set;
-
-/**
- * Callback for apps to implement handling for insertion of rich content. "Rich content" here refers
- * to both text and non-text content: plain text, styled text, HTML, images, videos, audio files,
- * etc.
- *
- * <p>This callback can be attached to different types of UI components. For editable
- * {@link android.widget.TextView} components, implementations should typically extend from
- * {@link TextViewRichContentReceiverCompat}.
- *
- * <p>Example implementation:<br>
- * <pre class="prettyprint">
- * public class MyRichContentReceiver extends TextViewRichContentReceiverCompat {
- *
- * private static final Set<String> SUPPORTED_MIME_TYPES = Collections.unmodifiableSet(
- * Set.of("text/*", "image/gif", "image/png", "image/jpg"));
- *
- * @NonNull
- * @Override
- * public Set<String> getSupportedMimeTypes() {
- * return SUPPORTED_MIME_TYPES;
- * }
- *
- * @Override
- * public boolean onReceive(@NonNull TextView textView, @NonNull ClipData clip,
- * int source, int flags) {
- * if (clip.getDescription().hasMimeType("image/*")) {
- * return receiveImage(textView, clip);
- * }
- * return super.onReceive(textView, clip, source);
- * }
- *
- * private boolean receiveImage(@NonNull TextView textView, @NonNull ClipData clip) {
- * // ... app-specific logic to handle the content URI in the clip ...
- * }
- * }
- * </pre>
- *
- * @param <T> The type of {@link View} with which this receiver can be associated.
- */
-public abstract class RichContentReceiverCompat<T extends View> {
- private static final String TAG = "RichContentReceiver";
-
- /**
- * Specifies the UI through which content is being inserted.
- */
- @IntDef(value = {SOURCE_CLIPBOARD, SOURCE_INPUT_METHOD})
- @Retention(RetentionPolicy.SOURCE)
- @interface Source {}
-
- /**
- * Specifies that the operation was triggered by a paste from the clipboard (e.g. "Paste" or
- * "Paste as plain text" action in the insertion/selection menu).
- */
- public static final int SOURCE_CLIPBOARD = 0;
-
- /**
- * Specifies that the operation was triggered from the soft keyboard (also known as input method
- * editor or IME). See https://developer.android.com/guide/topics/text/image-keyboard for more
- * info.
- */
- public static final int SOURCE_INPUT_METHOD = 1;
-
- /**
- * Flags to configure the insertion behavior.
- */
- @IntDef(flag = true, value = {FLAG_CONVERT_TO_PLAIN_TEXT})
- @Retention(RetentionPolicy.SOURCE)
- @interface Flags {}
-
- /**
- * Flag for {@link #onReceive} requesting that the content should be converted to plain text
- * prior to inserting.
- */
- public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1 << 0;
-
- /**
- * Insert the given clip.
- *
- * <p>For a UI component where this callback is set, this function will be invoked in the
- * following 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 ({@link InputConnection#commitContent})
- * </ol>
- *
- * <p>For text, if the view has a selection, the selection should be overwritten by the
- * clip; if there's no selection, this method should insert the content at the current
- * cursor position.
- *
- * <p>For rich content (e.g. an image), this function may insert the content inline, or it may
- * add the content as an attachment (could potentially go into a completely separate view).
- *
- * <p>This function may be invoked with a clip whose MIME type is not in the list of supported
- * types returned by {@link #getSupportedMimeTypes()}. This provides the opportunity to
- * implement custom fallback logic if desired.
- *
- * @param view The view where the content insertion was requested.
- * @param clip The clip to insert.
- * @param source The trigger of the operation.
- * @param flags Optional flags to configure the insertion behavior. Use 0 for default
- * behavior. See {@code FLAG_} constants on this class for other options.
- * @return Returns true if the clip was inserted.
- */
- public abstract boolean onReceive(@NonNull T view, @NonNull ClipData clip, @Source int source,
- @Flags int flags);
-
- /**
- * Returns the MIME types that can be handled by this callback.
- *
- * <p>Different platform features (e.g. pasting from the clipboard, inserting stickers from the
- * keyboard, etc) may use this function to conditionally alter their behavior. For example, the
- * keyboard may choose to hide its UI for inserting GIFs if the input field that has focus has
- * a {@link RichContentReceiverCompat} set and the MIME types returned from this function
- * don't include "image/gif".
- *
- * @return An immutable set with the MIME types supported by this callback. The returned
- * MIME types may contain wildcards such as "text/*", "image/*", etc.
- */
- @NonNull
- public abstract Set<String> getSupportedMimeTypes();
-
- /**
- * Returns true if the MIME type of the given clip is {@link #getSupportedMimeTypes() supported}
- * by this receiver.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- public final boolean supports(@NonNull ClipDescription description) {
- for (String supportedMimeType : getSupportedMimeTypes()) {
- if (description.hasMimeType(supportedMimeType)) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Populates {@code outAttrs.contentMimeTypes} with the supported MIME types of this receiver.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
- public final void populateEditorInfoContentMimeTypes(@Nullable InputConnection ic,
- @Nullable EditorInfo outAttrs) {
- if (ic == null || outAttrs == null) {
- return;
- }
- String[] mimeTypes = getSupportedMimeTypes().toArray(new String[0]);
- EditorInfoCompat.setContentMimeTypes(outAttrs, mimeTypes);
- }
-
- /**
- * Creates an {@link InputConnectionCompat.OnCommitContentListener} that uses this receiver
- * to insert content. The object returned by this function should be passed to
- * {@link InputConnectionCompat#createWrapper} when creating the {@link InputConnection} in
- * {@link View#onCreateInputConnection}.
- *
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
- @NonNull
- public final InputConnectionCompat.OnCommitContentListener buildOnCommitContentListener(
- @NonNull final T view) {
- return new InputConnectionCompat.OnCommitContentListener() {
- @Override
- public boolean onCommitContent(InputContentInfoCompat content, int flags,
- Bundle opts) {
- ClipDescription description = content.getDescription();
- if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
- try {
- content.requestPermission();
- } catch (Exception e) {
- Log.w(TAG, "Can't insert from IME; requestPermission() failed: " + e);
- return false; // Can't insert the content if we don't have permission
- }
- }
- ClipData clip = new ClipData(description,
- new ClipData.Item(content.getContentUri()));
- return onReceive(view, clip, SOURCE_INPUT_METHOD, 0);
- }
- };
- }
-}
diff --git a/core/core/src/main/java/androidx/core/widget/TextViewOnReceiveContentListener.java b/core/core/src/main/java/androidx/core/widget/TextViewOnReceiveContentListener.java
new file mode 100644
index 0000000..761cc82
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/widget/TextViewOnReceiveContentListener.java
@@ -0,0 +1,127 @@
+/*
+ * 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.core.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+import static androidx.core.view.ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT;
+import static androidx.core.view.ContentInfoCompat.SOURCE_INPUT_METHOD;
+
+import android.content.ClipData;
+import android.content.Context;
+import android.os.Build;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.Spanned;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.core.view.ContentInfoCompat;
+import androidx.core.view.ContentInfoCompat.Flags;
+import androidx.core.view.ContentInfoCompat.Source;
+import androidx.core.view.OnReceiveContentListener;
+
+/**
+ * Default implementation inserting content into editable {@link TextView} components. This class
+ * handles insertion of text (plain text, styled text, HTML, etc) but not images or other content.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP_PREFIX)
+public final class TextViewOnReceiveContentListener implements OnReceiveContentListener {
+ private static final String LOG_TAG = "ReceiveContent";
+
+ @Nullable
+ @Override
+ public ContentInfoCompat onReceiveContent(@NonNull View view,
+ @NonNull ContentInfoCompat payload) {
+ if (Log.isLoggable(LOG_TAG, Log.DEBUG)) {
+ Log.d(LOG_TAG, "onReceive: " + payload);
+ }
+ final @Source int source = payload.getSource();
+ if (source == SOURCE_INPUT_METHOD) {
+ // InputConnection.commitContent() should only be used for non-text input which is not
+ // supported by the default implementation.
+ return payload;
+ }
+
+ // The code here follows the platform logic in TextView:
+ // https://cs.android.com/android/_/android/platform/frameworks/base/+/9fefb65aa9e7beae9ca8306b925b9fbfaeffecc9:core/java/android/widget/TextView.java;l=12644
+ // In particular, multiple items within the given ClipData will trigger separate calls to
+ // replace/insert. This is to preserve the platform behavior with respect to TextWatcher
+ // notifications fired from SpannableStringBuilder when replace/insert is called.
+ final ClipData clip = payload.getClip();
+ final @Flags int flags = payload.getFlags();
+ final TextView textView = (TextView) view;
+ final Editable editable = (Editable) textView.getText();
+ final Context context = textView.getContext();
+ boolean didFirst = false;
+ for (int i = 0; i < clip.getItemCount(); i++) {
+ CharSequence paste;
+ if (Build.VERSION.SDK_INT >= 16) {
+ paste = CoerceToTextApi16Impl.coerce(context, clip.getItemAt(i), flags);
+ } else {
+ paste = CoerceToTextImpl.coerce(context, clip.getItemAt(i), flags);
+ }
+ if (paste != null) {
+ if (!didFirst) {
+ final int selStart = Selection.getSelectionStart(editable);
+ final int selEnd = Selection.getSelectionEnd(editable);
+ final int start = Math.max(0, Math.min(selStart, selEnd));
+ final int end = Math.max(0, Math.max(selStart, selEnd));
+ Selection.setSelection(editable, end);
+ editable.replace(start, end, paste);
+ didFirst = true;
+ } else {
+ editable.insert(Selection.getSelectionEnd(editable), "\n");
+ editable.insert(Selection.getSelectionEnd(editable), paste);
+ }
+ }
+ }
+ return null;
+ }
+
+ @RequiresApi(16) // For ClipData.Item.coerceToStyledText()
+ private static final class CoerceToTextApi16Impl {
+ private CoerceToTextApi16Impl() {}
+
+ static CharSequence coerce(Context context, ClipData.Item item, @Flags int flags) {
+ if ((flags & FLAG_CONVERT_TO_PLAIN_TEXT) != 0) {
+ CharSequence text = item.coerceToText(context);
+ return (text instanceof Spanned) ? text.toString() : text;
+ } else {
+ return item.coerceToStyledText(context);
+ }
+ }
+ }
+
+ private static final class CoerceToTextImpl {
+ private CoerceToTextImpl() {}
+
+ static CharSequence coerce(Context context, ClipData.Item item, @Flags int flags) {
+ CharSequence text = item.coerceToText(context);
+ if ((flags & FLAG_CONVERT_TO_PLAIN_TEXT) != 0 && text instanceof Spanned) {
+ text = text.toString();
+ }
+ return text;
+ }
+ }
+}
diff --git a/core/core/src/main/java/androidx/core/widget/TextViewRichContentReceiverCompat.java b/core/core/src/main/java/androidx/core/widget/TextViewRichContentReceiverCompat.java
deleted file mode 100644
index 31c51b1..0000000
--- a/core/core/src/main/java/androidx/core/widget/TextViewRichContentReceiverCompat.java
+++ /dev/null
@@ -1,104 +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.core.widget;
-
-import android.content.ClipData;
-import android.content.Context;
-import android.os.Build;
-import android.text.Editable;
-import android.text.Selection;
-import android.text.Spanned;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-
-import java.util.Collections;
-import java.util.Set;
-
-/**
- * Base implementation of {@link RichContentReceiverCompat} for editable {@link TextView}
- * components.
- *
- * <p>This class handles insertion of text (plain text, styled text, HTML, etc) but not images or
- * other rich content. It should be used as a base class when implementing a custom
- * {@link RichContentReceiverCompat}, to provide consistent behavior for insertion of text while
- * implementing custom behavior for insertion of other content (images, etc).
- *
- * <p>See {@link RichContentReceiverCompat} for an example of how to implement a custom receiver.
- */
-public abstract class TextViewRichContentReceiverCompat extends
- RichContentReceiverCompat<TextView> {
-
- private static final Set<String> MIME_TYPES_ALL_TEXT = Collections.singleton("text/*");
-
- /**
- * {@inheritDoc}
- */
- @Override
- @NonNull
- public Set<String> getSupportedMimeTypes() {
- return MIME_TYPES_ALL_TEXT;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean onReceive(@NonNull TextView textView, @NonNull ClipData clip,
- @Source int source, @Flags int flags) {
- if (source == SOURCE_INPUT_METHOD && !supports(clip.getDescription())) {
- return false;
- }
-
- // The code here follows the platform logic in TextView:
- // https://cs.android.com/android/_/android/platform/frameworks/base/+/9fefb65aa9e7beae9ca8306b925b9fbfaeffecc9:core/java/android/widget/TextView.java;l=12644
- // In particular, multiple items within the given ClipData will trigger separate calls to
- // replace/insert. This is to preserve the platform behavior with respect to TextWatcher
- // notifications fired from SpannableStringBuilder when replace/insert is called.
- final Editable editable = (Editable) textView.getText();
- final Context context = textView.getContext();
- boolean didFirst = false;
- for (int i = 0; i < clip.getItemCount(); i++) {
- CharSequence paste;
- if ((flags & FLAG_CONVERT_TO_PLAIN_TEXT) != 0) {
- paste = clip.getItemAt(i).coerceToText(context);
- paste = (paste instanceof Spanned) ? paste.toString() : paste;
- } else {
- if (Build.VERSION.SDK_INT >= 16) {
- paste = clip.getItemAt(i).coerceToStyledText(context);
- } else {
- paste = clip.getItemAt(i).coerceToText(context);
- }
- }
- if (paste != null) {
- if (!didFirst) {
- final int selStart = Selection.getSelectionStart(editable);
- final int selEnd = Selection.getSelectionEnd(editable);
- final int start = Math.max(0, Math.min(selStart, selEnd));
- final int end = Math.max(0, Math.max(selStart, selEnd));
- Selection.setSelection(editable, end);
- editable.replace(start, end, paste);
- didFirst = true;
- } else {
- editable.insert(Selection.getSelectionEnd(editable), "\n");
- editable.insert(Selection.getSelectionEnd(editable), paste);
- }
- }
- }
- return didFirst;
- }
-}
diff --git a/core/core/src/main/res/values/ids.xml b/core/core/src/main/res/values/ids.xml
index 5147433..b842d05 100644
--- a/core/core/src/main/res/values/ids.xml
+++ b/core/core/src/main/res/values/ids.xml
@@ -30,6 +30,8 @@
<item name="tag_accessibility_clickable_spans" type="id"/>
<item name="tag_accessibility_actions" type="id"/>
<item name="tag_state_description" type="id"/>
+ <item name="tag_on_receive_content_listener" type="id"/>
+ <item name="tag_on_receive_content_mime_types" type="id"/>
<item name="accessibility_custom_action_0" type="id"/>
<item name="accessibility_custom_action_1" type="id"/>
<item name="accessibility_custom_action_2" type="id"/>
diff --git a/customview/customview/api/current.txt b/customview/customview/api/current.txt
index 2834ec2..b7566cb 100644
--- a/customview/customview/api/current.txt
+++ b/customview/customview/api/current.txt
@@ -38,6 +38,7 @@
method protected void onVirtualViewKeyboardFocusChanged(int, boolean);
method public final boolean requestKeyboardFocusForVirtualView(int);
method public final boolean sendEventForVirtualView(int, int);
+ method public final void setBoundsInScreenFromBoundsInParent(androidx.core.view.accessibility.AccessibilityNodeInfoCompat, android.graphics.Rect);
field public static final int HOST_ID = -1; // 0xffffffff
field public static final int INVALID_ID = -2147483648; // 0x80000000
}
diff --git a/customview/customview/api/public_plus_experimental_current.txt b/customview/customview/api/public_plus_experimental_current.txt
index 2834ec2..b7566cb 100644
--- a/customview/customview/api/public_plus_experimental_current.txt
+++ b/customview/customview/api/public_plus_experimental_current.txt
@@ -38,6 +38,7 @@
method protected void onVirtualViewKeyboardFocusChanged(int, boolean);
method public final boolean requestKeyboardFocusForVirtualView(int);
method public final boolean sendEventForVirtualView(int, int);
+ method public final void setBoundsInScreenFromBoundsInParent(androidx.core.view.accessibility.AccessibilityNodeInfoCompat, android.graphics.Rect);
field public static final int HOST_ID = -1; // 0xffffffff
field public static final int INVALID_ID = -2147483648; // 0x80000000
}
diff --git a/customview/customview/api/restricted_current.txt b/customview/customview/api/restricted_current.txt
index 2834ec2..b7566cb 100644
--- a/customview/customview/api/restricted_current.txt
+++ b/customview/customview/api/restricted_current.txt
@@ -38,6 +38,7 @@
method protected void onVirtualViewKeyboardFocusChanged(int, boolean);
method public final boolean requestKeyboardFocusForVirtualView(int);
method public final boolean sendEventForVirtualView(int, int);
+ method public final void setBoundsInScreenFromBoundsInParent(androidx.core.view.accessibility.AccessibilityNodeInfoCompat, android.graphics.Rect);
field public static final int HOST_ID = -1; // 0xffffffff
field public static final int INVALID_ID = -2147483648; // 0x80000000
}
diff --git a/customview/customview/src/androidTest/java/androidx/customview/widget/ExploreByTouchHelperTest.java b/customview/customview/src/androidTest/java/androidx/customview/widget/ExploreByTouchHelperTest.java
index 1e11425..ebaa94a 100644
--- a/customview/customview/src/androidTest/java/androidx/customview/widget/ExploreByTouchHelperTest.java
+++ b/customview/customview/src/androidTest/java/androidx/customview/widget/ExploreByTouchHelperTest.java
@@ -17,11 +17,9 @@
package androidx.customview.widget;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import android.graphics.Rect;
-import android.graphics.RectF;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
@@ -61,70 +59,94 @@
@Test
@UiThreadTest
- public void testBoundsInScreen() {
- final ExploreByTouchHelper helper = new ParentBoundsHelper(mHost);
+ public void testAssignBoundsInParent() {
+ final TwoNestedViewHelper boundsInParentOnlyHelper = new ParentBoundsHelper(mHost);
+ testBounds(boundsInParentOnlyHelper);
+ }
+
+ @Test
+ @UiThreadTest
+ public void testAssignBoundsInScreen() {
+ final TwoNestedViewHelper boundsInScreenOnlyHelper = new ScreenBoundsHelper(mHost);
+ testBounds(boundsInScreenOnlyHelper);
+ }
+
+ @Test
+ @UiThreadTest
+ public void testAssignBoundsInScreenAndParent() {
+ final TwoNestedViewHelper boundsInScreenAndParentHelper =
+ new ParentAndScreenBoundsHelper(mHost);
+ testBounds(boundsInScreenAndParentHelper);
+ }
+
+ private void testBounds(TwoNestedViewHelper helper) {
ViewCompat.setAccessibilityDelegate(mHost, helper);
-
- final AccessibilityNodeInfoCompat node =
- helper.getAccessibilityNodeProvider(mHost).createAccessibilityNodeInfo(1);
- assertNotNull(node);
-
- final Rect hostBounds = new Rect();
- mHost.getLocalVisibleRect(hostBounds);
- assertFalse("Host has not been laid out", hostBounds.isEmpty());
-
- final Rect nodeBoundsInParent = new Rect();
- node.getBoundsInParent(nodeBoundsInParent);
- assertEquals("Wrong bounds in parent", hostBounds, nodeBoundsInParent);
-
- final Rect hostBoundsOnScreen = getBoundsOnScreen(mHost);
- final Rect nodeBoundsInScreen = new Rect();
- node.getBoundsInScreen(nodeBoundsInScreen);
- assertEquals("Wrong bounds in screen", hostBoundsOnScreen, nodeBoundsInScreen);
-
- final int scrollX = 100;
- final int scrollY = 50;
- mHost.scrollTo(scrollX, scrollY);
-
- // Generate a node for the new position.
- final AccessibilityNodeInfoCompat scrolledNode =
- helper.getAccessibilityNodeProvider(mHost).createAccessibilityNodeInfo(1);
- assertNotNull(scrolledNode);
-
- // Bounds in parent should not be affected by visibility.
- final Rect scrolledNodeBoundsInParent = new Rect();
- scrolledNode.getBoundsInParent(scrolledNodeBoundsInParent);
- assertEquals("Wrong bounds in parent after scrolling",
- hostBounds, scrolledNodeBoundsInParent);
-
- final Rect expectedBoundsInScreen = new Rect(hostBoundsOnScreen);
- expectedBoundsInScreen.offset(-scrollX, -scrollY);
- expectedBoundsInScreen.intersect(hostBoundsOnScreen);
- scrolledNode.getBoundsInScreen(nodeBoundsInScreen);
- assertEquals("Wrong bounds in screen after scrolling",
- expectedBoundsInScreen, nodeBoundsInScreen);
-
+ testBounds(helper, 0);
+ testBounds(helper, 1);
+ mHost.scrollTo(100, 50);
+ testBounds(helper, 0);
+ testBounds(helper, 1);
+ mHost.scrollTo(0, 0);
ViewCompat.setAccessibilityDelegate(mHost, null);
}
+
+ private void testBounds(TwoNestedViewHelper helper, int virtualViewId) {
+ AccessibilityNodeInfoCompat node =
+ helper.getAccessibilityNodeProvider(mHost).createAccessibilityNodeInfo(
+ virtualViewId);
+ assertNotNull(node);
+
+ VirtualItem item = helper.mVirtualItems[virtualViewId];
+ final Rect nodeBoundsInParent = new Rect();
+ node.getBoundsInParent(nodeBoundsInParent);
+ assertEquals("Wrong bounds in parent", item.mBoundsInParent, nodeBoundsInParent);
+
+ final Rect expectedNodeBoundsInScreen = getBoundsOnScreen(helper, virtualViewId,
+ item.mParentId);
+ final Rect nodeBoundsInScreen = new Rect();
+ node.getBoundsInScreen(nodeBoundsInScreen);
+ assertEquals("Wrong bounds in screen", expectedNodeBoundsInScreen,
+ nodeBoundsInScreen);
+
+ node.recycle();
+ }
+
+ private Rect getBoundsOnScreen(TwoNestedViewHelper helper, int virtualViewId,
+ int virtualParentId) {
+ final Rect boundsOnScreen = new Rect();
+ boundsOnScreen.set(helper.mVirtualItems[virtualViewId].mBoundsInParent);
+ if (virtualParentId != ExploreByTouchHelper.HOST_ID) {
+ boundsOnScreen.offset(helper.mVirtualItems[virtualParentId].mBoundsInParent.left,
+ helper.mVirtualItems[virtualParentId].mBoundsInParent.top);
+ }
+ final int[] tempLocation = new int[2];
+ mHost.getLocationOnScreen(tempLocation);
+ boundsOnScreen.offset(tempLocation[0] - mHost.getScrollX(),
+ tempLocation[1] - mHost.getScrollY());
+ final Rect tempVisibleRect = new Rect();
+ mHost.getLocalVisibleRect(tempVisibleRect);
+ tempVisibleRect.offset(tempLocation[0] - mHost.getScrollX(),
+ tempLocation[1] - mHost.getScrollY());
+ boundsOnScreen.intersect(tempVisibleRect);
+ return boundsOnScreen;
+ }
+
@Test
@UiThreadTest
public void testMoveFocusToNextVirtualId() {
- final ExploreByTouchHelper helper = new FocusTouchHelper(mHost);
-
+ final ExploreByTouchHelper helper = new TwoNestedViewHelper(mHost);
ViewCompat.setAccessibilityDelegate(mHost, helper);
+ boolean moveFocusToId0 = helper.dispatchKeyEvent(
+ new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB));
+ assertEquals(0, helper.getKeyboardFocusedVirtualViewId());
+ assertEquals(true, moveFocusToId0);
+
boolean moveFocusToId1 = helper.dispatchKeyEvent(
new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB));
assertEquals(1, helper.getKeyboardFocusedVirtualViewId());
assertEquals(true, moveFocusToId1);
- // moveFocus should move focus to the node with id 5
- boolean moveFocusToId5 = helper.dispatchKeyEvent(
- new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB));
- assertEquals(5, helper.getKeyboardFocusedVirtualViewId());
- assertEquals(true, moveFocusToId5);
-
- // moveFocus should not return true if the node has id INVALID_ID.
boolean moveFocusToInvalidId = helper.dispatchKeyEvent(
new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB));
assertEquals(ExploreByTouchHelper.INVALID_ID, helper.getKeyboardFocusedVirtualViewId());
@@ -133,95 +155,151 @@
ViewCompat.setAccessibilityDelegate(mHost, null);
}
- private static Rect getBoundsOnScreen(View v) {
- final int[] tempLocation = new int[2];
- final Rect hostBoundsOnScreen = new Rect(0, 0, v.getWidth(), v.getHeight());
- v.getLocationOnScreen(tempLocation);
- hostBoundsOnScreen.offset(tempLocation[0], tempLocation[1]);
- return hostBoundsOnScreen;
+ @Test
+ @UiThreadTest
+ public void testMoveFocusDirection() {
+ final ExploreByTouchHelper helper = new TwoNestedViewHelper(mHost);
+ ViewCompat.setAccessibilityDelegate(mHost, helper);
+ helper.requestKeyboardFocusForVirtualView(0);
+
+ boolean moveFocusUp = helper.dispatchKeyEvent(
+ new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_UP));
+ assertEquals(ExploreByTouchHelper.INVALID_ID, helper.getKeyboardFocusedVirtualViewId());
+ assertEquals(false, moveFocusUp);
+
+ boolean moveFocusDown = helper.dispatchKeyEvent(
+ new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_DOWN));
+ assertEquals(0, helper.getKeyboardFocusedVirtualViewId());
+ assertEquals(true, moveFocusDown);
+
+ ViewCompat.setAccessibilityDelegate(mHost, null);
}
/**
- * An extension of ExploreByTouchHelper that contains a single virtual view
- * whose bounds match the host view.
+ * An extension of ExploreByTouchHelper that contains 2 nested virtual view
+ * and specify {@link AccessibilityNodeInfoCompat#setBoundsInParent}.
*/
- private static class ParentBoundsHelper extends ExploreByTouchHelper {
- private final View mHost;
+ private static class ParentBoundsHelper extends TwoNestedViewHelper {
ParentBoundsHelper(View host) {
super(host);
-
- mHost = host;
}
@Override
- protected int getVirtualViewAt(float x, float y) {
- return 1;
- }
-
- @Override
- protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
- virtualViewIds.add(1);
- }
-
- @Override
- protected void onPopulateNodeForVirtualView(int virtualViewId,
- @NonNull AccessibilityNodeInfoCompat node) {
- if (virtualViewId == 1) {
- node.setContentDescription("test");
-
- final Rect hostBounds = new Rect(0, 0, mHost.getWidth(), mHost.getHeight());
- node.setBoundsInParent(hostBounds);
- }
- }
-
- @Override
- protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
- Bundle arguments) {
- return false;
+ protected void onPopulateNodeForVirtualView(
+ int virtualViewId, @NonNull AccessibilityNodeInfoCompat node) {
+ populateNodeForVirtualView(/* setBoundsFromParent= */true,
+ /* setBoundsFromScreen= */ false, virtualViewId, node);
}
}
/**
- * An extension of ExploreByTouchHelper that contains two virtual views to test moving focus.
+ * An extension of ExploreByTouchHelper that contains 2 nested virtual view
+ * and specify {@link AccessibilityNodeInfoCompat#setBoundsInScreen} by calling
+ * {@link ExploreByTouchHelper#setBoundsInScreenFromBoundsInParent}.
*/
- private static class FocusTouchHelper extends ExploreByTouchHelper {
- private final View mHost;
+ private static class ScreenBoundsHelper extends TwoNestedViewHelper {
- FocusTouchHelper(View host) {
+ ScreenBoundsHelper(View host) {
+ super(host);
+ }
+
+ @Override
+ protected void onPopulateNodeForVirtualView(
+ int virtualViewId, @NonNull AccessibilityNodeInfoCompat node) {
+ populateNodeForVirtualView(/* setBoundsFromParent= */false,
+ /* setBoundsFromScreen= */ true, virtualViewId, node);
+ }
+ }
+
+ /**
+ * An extension of ExploreByTouchHelper that contains 2 nested virtual view
+ * and specify {@link AccessibilityNodeInfoCompat#setBoundsInParent}
+ * and {@link AccessibilityNodeInfoCompat#setBoundsInScreen} by calling
+ * {@link ExploreByTouchHelper#setBoundsInScreenFromBoundsInParent}.
+ */
+ private static class ParentAndScreenBoundsHelper extends TwoNestedViewHelper {
+
+ ParentAndScreenBoundsHelper(View host) {
+ super(host);
+ }
+
+ @Override
+ protected void onPopulateNodeForVirtualView(
+ int virtualViewId, @NonNull AccessibilityNodeInfoCompat node) {
+ populateNodeForVirtualView(/* setBoundsFromParent= */true,
+ /* setBoundsFromScreen= */ true, virtualViewId, node);
+ }
+ }
+
+ private static class VirtualItem {
+ private int mParentId;
+ private Rect mBoundsInParent;
+ private String mText;
+
+ VirtualItem(int parentId, String text, Rect boundsInParent) {
+ this.mParentId = parentId;
+ this.mBoundsInParent = boundsInParent;
+ this.mText = text;
+ }
+ }
+
+ /**
+ * An extension of ExploreByTouchHelper that contains 2 nested virtual views.
+ * Host view contains 1 child "bottom" and "bottom" contains one child
+ * "nested-bottom-right".
+ */
+ private static class TwoNestedViewHelper extends ExploreByTouchHelper {
+ private final View mHost;
+ protected VirtualItem[] mVirtualItems = new VirtualItem[2];
+
+ TwoNestedViewHelper(View host) {
super(host);
mHost = host;
+ mVirtualItems[0] = new VirtualItem(ExploreByTouchHelper.HOST_ID, "bottom",
+ new Rect(0, mHost.getHeight() / 2,
+ mHost.getWidth(), mHost.getHeight()));
+ mVirtualItems[1] = new VirtualItem(0, "nested-bottom-right",
+ new Rect(mHost.getWidth() / 2, 0,
+ mHost.getWidth(), mHost.getHeight() / 2));
}
@Override
protected int getVirtualViewAt(float x, float y) {
- RectF topHalf = new RectF();
- topHalf.set(0, 0, mHost.getWidth(), mHost.getHeight() / 2);
- if (topHalf.contains(x, y)) {
+ if (x < mHost.getWidth() / 2 && y > mHost.getHeight() / 2) {
+ return 0;
+ } else if (x > mHost.getWidth() / 2 && y > mHost.getHeight() / 2) {
return 1;
}
- return 5;
+ return -1;
}
@Override
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
+ virtualViewIds.add(0);
virtualViewIds.add(1);
- virtualViewIds.add(5);
}
@Override
- protected void onPopulateNodeForVirtualView(int virtualViewId,
+ protected void onPopulateNodeForVirtualView(
+ int virtualViewId, @NonNull AccessibilityNodeInfoCompat node) {
+ populateNodeForVirtualView(/* setBoundsFromParent= */false,
+ /* setBoundsFromScreen= */ true, virtualViewId, node);
+ }
+
+ protected void populateNodeForVirtualView(boolean setBoundsFromParent,
+ boolean setBoundsFromScreen, int virtualViewId,
@NonNull AccessibilityNodeInfoCompat node) {
- if (virtualViewId == 1) {
- node.setContentDescription("test 1");
- final Rect hostBounds = new Rect(0, 0, mHost.getWidth(), mHost.getHeight() / 2);
- node.setBoundsInParent(hostBounds);
- }
- if (virtualViewId == 5) {
- node.setContentDescription("test 5");
- final Rect hostBounds =
- new Rect(0, mHost.getHeight() / 2, mHost.getWidth(), mHost.getHeight());
- node.setBoundsInParent(hostBounds);
+ if (virtualViewId <= mVirtualItems.length) {
+ int index = virtualViewId;
+ node.setContentDescription(mVirtualItems[index].mText);
+ node.setParent(mHost, mVirtualItems[index].mParentId);
+ if (setBoundsFromParent) {
+ node.setBoundsInParent(mVirtualItems[index].mBoundsInParent);
+ }
+ if (setBoundsFromScreen) {
+ setBoundsInScreenFromBoundsInParent(node, mVirtualItems[index].mBoundsInParent);
+ }
}
}
@@ -230,6 +308,5 @@
Bundle arguments) {
return false;
}
-
}
}
diff --git a/customview/customview/src/main/java/androidx/customview/widget/ExploreByTouchHelper.java b/customview/customview/src/main/java/androidx/customview/widget/ExploreByTouchHelper.java
index ae399dd..adc7915 100644
--- a/customview/customview/src/main/java/androidx/customview/widget/ExploreByTouchHelper.java
+++ b/customview/customview/src/main/java/androidx/customview/widget/ExploreByTouchHelper.java
@@ -97,7 +97,7 @@
private static final String DEFAULT_CLASS_NAME = "android.view.View";
/** Default bounds used to determine if the client didn't set any. */
- private static final Rect INVALID_PARENT_BOUNDS = new Rect(
+ private static final Rect INVALID_BOUNDS = new Rect(
Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE);
// Temporary, reusable data structures.
@@ -324,9 +324,9 @@
* @param virtualViewId the identifier of the virtual view
* @param outBounds the rect to populate with virtual view bounds
*/
- private void getBoundsInParent(int virtualViewId, Rect outBounds) {
+ private void getBoundsInScreen(int virtualViewId, Rect outBounds) {
final AccessibilityNodeInfoCompat node = obtainAccessibilityNodeInfo(virtualViewId);
- node.getBoundsInParent(outBounds);
+ node.getBoundsInScreen(outBounds);
}
/**
@@ -336,7 +336,7 @@
new FocusStrategy.BoundsAdapter<AccessibilityNodeInfoCompat>() {
@Override
public void obtainBounds(AccessibilityNodeInfoCompat node, Rect outBounds) {
- node.getBoundsInParent(outBounds);
+ node.getBoundsInScreen(outBounds);
}
};
@@ -392,7 +392,7 @@
final Rect selectedRect = new Rect();
if (mKeyboardFocusedVirtualViewId != INVALID_ID) {
// Focus is moving from a virtual view within the host.
- getBoundsInParent(mKeyboardFocusedVirtualViewId, selectedRect);
+ getBoundsInScreen(mKeyboardFocusedVirtualViewId, selectedRect);
} else if (previouslyFocusedRect != null) {
// Focus is moving from a real view outside the host.
selectedRect.set(previouslyFocusedRect);
@@ -772,16 +772,6 @@
* <li>{@link AccessibilityNodeInfoCompat#setParent(View)}
* <li>{@link AccessibilityNodeInfoCompat#setSource(View, int)}
* <li>{@link AccessibilityNodeInfoCompat#setVisibleToUser}
- * <li>{@link AccessibilityNodeInfoCompat#setBoundsInScreen(Rect)}
- * </ul>
- * <p>
- * Uses the bounds of the parent view and the parent-relative bounding
- * rectangle specified by
- * {@link AccessibilityNodeInfoCompat#getBoundsInParent} to automatically
- * update the following properties:
- * <ul>
- * <li>{@link AccessibilityNodeInfoCompat#setVisibleToUser}
- * <li>{@link AccessibilityNodeInfoCompat#setBoundsInParent}
* </ul>
*
* @param virtualViewId the virtual view id for item for which to construct
@@ -797,8 +787,8 @@
node.setFocusable(true);
node.setClassName(DEFAULT_CLASS_NAME);
- node.setBoundsInParent(INVALID_PARENT_BOUNDS);
- node.setBoundsInScreen(INVALID_PARENT_BOUNDS);
+ node.setBoundsInParent(INVALID_BOUNDS);
+ node.setBoundsInScreen(INVALID_BOUNDS);
node.setParent(mHost);
// Allow the client to populate the node.
@@ -811,8 +801,10 @@
}
node.getBoundsInParent(mTempParentRect);
- if (mTempParentRect.equals(INVALID_PARENT_BOUNDS)) {
- throw new RuntimeException("Callbacks must set parent bounds in "
+ node.getBoundsInScreen(mTempScreenRect);
+ if (mTempParentRect.equals(INVALID_BOUNDS) && mTempScreenRect.equals(
+ INVALID_BOUNDS)) {
+ throw new RuntimeException("Callbacks must set parent bounds or screen bounds in "
+ "populateNodeForVirtualViewId()");
}
@@ -850,32 +842,9 @@
mHost.getLocationOnScreen(mTempGlobalRect);
- // If not explicitly specified, calculate screen-relative bounds and
- // offset for scroll position based on bounds in parent.
- node.getBoundsInScreen(mTempScreenRect);
- if (mTempScreenRect.equals(INVALID_PARENT_BOUNDS)) {
- node.getBoundsInParent(mTempScreenRect);
-
- // If there is a parent node, adjust bounds based on the parent node.
- if (node.mParentVirtualDescendantId != HOST_ID) {
- AccessibilityNodeInfoCompat parentNode = AccessibilityNodeInfoCompat.obtain();
- // Walk up the node tree to adjust the screen rect.
- for (int virtualDescendantId = node.mParentVirtualDescendantId;
- virtualDescendantId != HOST_ID;
- virtualDescendantId = parentNode.mParentVirtualDescendantId) {
- // Reset the values in the parent node we'll be using.
- parentNode.setParent(mHost, HOST_ID);
- parentNode.setBoundsInParent(INVALID_PARENT_BOUNDS);
- // Adjust the bounds for the parent node.
- onPopulateNodeForVirtualView(virtualDescendantId, parentNode);
- parentNode.getBoundsInParent(mTempParentRect);
- mTempScreenRect.offset(mTempParentRect.left, mTempParentRect.top);
- }
- parentNode.recycle();
- }
- // Adjust the rect for the host view's location.
- mTempScreenRect.offset(mTempGlobalRect[0] - mHost.getScrollX(),
- mTempGlobalRect[1] - mHost.getScrollY());
+ if (mTempScreenRect.equals(INVALID_BOUNDS)) {
+ setBoundsInScreenFromBoundsInParent(node, mTempParentRect);
+ node.getBoundsInScreen(mTempScreenRect);
}
if (mHost.getLocalVisibleRect(mTempVisibleRect)) {
@@ -884,7 +853,6 @@
final boolean intersects = mTempScreenRect.intersect(mTempVisibleRect);
if (intersects) {
node.setBoundsInScreen(mTempScreenRect);
-
if (isVisibleToUser(mTempScreenRect)) {
node.setVisibleToUser(true);
}
@@ -924,15 +892,15 @@
/**
* Computes whether the specified {@link Rect} intersects with the visible
- * portion of its parent {@link View}. Modifies {@code localRect} to contain
+ * portion of its parent {@link View}. Modifies {@code screenRect} to contain
* only the visible portion.
*
- * @param localRect a rectangle in local (parent) coordinates
+ * @param screenRect a rectangle in screen coordinates
* @return whether the specified {@link Rect} is visible on the screen
*/
- private boolean isVisibleToUser(Rect localRect) {
+ private boolean isVisibleToUser(Rect screenRect) {
// Missing or empty bounds mean this view is not visible.
- if ((localRect == null) || localRect.isEmpty()) {
+ if ((screenRect == null) || screenRect.isEmpty()) {
return false;
}
@@ -1064,6 +1032,46 @@
}
/**
+ * Calculates and assigns screen-relative bounds based on bounds in parent. Instead
+ * of calling the deprecated {@link AccessibilityNodeInfoCompat#setBoundsInParent(Rect)}, it
+ * provides a convenient method to calculate and assign
+ * {@link AccessibilityNodeInfoCompat#setBoundsInScreen(Rect)} based on {@code boundsInParent}.
+ *
+ * @param node The node to populate
+ * @param boundsInParent The node bounds in the viewParent's coordinates.
+ */
+ public final void setBoundsInScreenFromBoundsInParent(@NonNull AccessibilityNodeInfoCompat node,
+ @NonNull Rect boundsInParent) {
+ node.setBoundsInParent(boundsInParent);
+ Rect screenRect = new Rect();
+ screenRect.set(boundsInParent);
+
+ // If there is a parent node, adjust bounds based on the parent node.
+ if (node.mParentVirtualDescendantId != HOST_ID) {
+ AccessibilityNodeInfoCompat parentNode = AccessibilityNodeInfoCompat.obtain();
+ Rect tempParentRect = new Rect();
+ // Walk up the node tree to adjust the screen rect.
+ for (int virtualDescendantId = node.mParentVirtualDescendantId;
+ virtualDescendantId != HOST_ID;
+ virtualDescendantId = parentNode.mParentVirtualDescendantId) {
+ // Reset the values in the parent node we'll be using.
+ parentNode.setParent(mHost, HOST_ID);
+ parentNode.setBoundsInParent(INVALID_BOUNDS);
+ // Adjust the bounds for the parent node.
+ onPopulateNodeForVirtualView(virtualDescendantId, parentNode);
+ parentNode.getBoundsInParent(tempParentRect);
+ screenRect.offset(tempParentRect.left, tempParentRect.top);
+ }
+ parentNode.recycle();
+ }
+ // Adjust the rect for the host view's location.
+ mHost.getLocationOnScreen(mTempGlobalRect);
+ screenRect.offset(mTempGlobalRect[0] - mHost.getScrollX(),
+ mTempGlobalRect[1] - mHost.getScrollY());
+ node.setBoundsInScreen(screenRect);
+ }
+
+ /**
* Provides a mapping between view-relative coordinates and logical
* items.
*
@@ -1144,8 +1152,10 @@
* <li>event text, see
* {@link AccessibilityNodeInfoCompat#setText(CharSequence)} or
* {@link AccessibilityNodeInfoCompat#setContentDescription(CharSequence)}
- * <li>bounds in parent coordinates, see
- * {@link AccessibilityNodeInfoCompat#setBoundsInParent(Rect)}
+ * <li>bounds in screen coordinates, see
+ * {@link AccessibilityNodeInfoCompat#setBoundsInScreen(Rect)} and
+ * {@link ExploreByTouchHelper#setBoundsInScreenFromBoundsInParent
+ * (AccessibilityNodeInfoCompat, Rect)}
* </ul>
* <p>
* The helper class automatically populates the following fields with
@@ -1176,8 +1186,6 @@
* {@link AccessibilityNodeInfoCompat#setAccessibilityFocused(boolean)}
* <li>keyboard focus, computed based on internal helper state, see
* {@link AccessibilityNodeInfoCompat#setFocused(boolean)}
- * <li>bounds in screen coordinates, computed based on host view bounds,
- * see {@link AccessibilityNodeInfoCompat#setBoundsInScreen(Rect)}
* </ul>
* <p>
* Additionally, the helper class automatically handles keyboard focus and
diff --git a/datastore/datastore-core/src/main/java/androidx/datastore/core/DataMigration.kt b/datastore/datastore-core/src/main/java/androidx/datastore/core/DataMigration.kt
index c858653..4318b91 100644
--- a/datastore/datastore-core/src/main/java/androidx/datastore/core/DataMigration.kt
+++ b/datastore/datastore-core/src/main/java/androidx/datastore/core/DataMigration.kt
@@ -35,6 +35,9 @@
*
* Note that this will always be called before each call to [migrate].
*
+ * Note that accessing any data from DataStore directly from inside this function will result
+ * in deadlock, since DataStore doesn't return data until all migrations complete.
+ *
* @param currentData the current data (which might already populated from previous runs of this
* or other migrations)
*/
@@ -49,6 +52,9 @@
*
* Note that this will always be called before a call to [cleanUp].
*
+ * Note that accessing any data from DataStore directly from inside this function will result
+ * in deadlock, since DataStore doesn't return data until all migrations complete.
+ *
* @param currentData the current data (it might be populated from other migrations or from
* manual changes before this migration was added to the app)
* @return The migrated data.
@@ -61,6 +67,10 @@
* back to the DataStore call that triggered the migration and future calls to DataStore will
* result in DataMigrations being attempted again. This method may be run multiple times when
* any failure is encountered.
+ *
+ * This is useful for cleaning up files or data outside of DataStore and accessing any
+ * data from DataStore directly from inside this function will result in deadlock, since
+ * DataStore doesn't return data until all migrations complete.
*/
public suspend fun cleanUp()
}
\ No newline at end of file
diff --git a/datastore/datastore-rxjava2/api/current.txt b/datastore/datastore-rxjava2/api/current.txt
index 42e32ea..b52a1fc 100644
--- a/datastore/datastore-rxjava2/api/current.txt
+++ b/datastore/datastore-rxjava2/api/current.txt
@@ -1,20 +1,37 @@
// Signature format: 4.0
package androidx.datastore.rxjava2 {
+ public interface RxDataMigration<T> {
+ method public io.reactivex.Completable cleanUp();
+ method public io.reactivex.Single<T!> migrate(T?);
+ method public io.reactivex.Single<java.lang.Boolean!> shouldMigrate(T?);
+ }
+
public final class RxDataStore {
method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Flowable<T> data(androidx.datastore.core.DataStore<T>);
method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Single<T> updateDataAsync(androidx.datastore.core.DataStore<T>, io.reactivex.functions.Function<T,io.reactivex.Single<T>> transform);
}
public final class RxDataStoreBuilder<T> {
- ctor public RxDataStoreBuilder();
+ ctor public RxDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile, androidx.datastore.core.Serializer<T> serializer);
+ ctor public RxDataStoreBuilder(android.content.Context context, String fileName, androidx.datastore.core.Serializer<T> serializer);
method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> addDataMigration(androidx.datastore.core.DataMigration<T> dataMigration);
+ method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> addRxDataMigration(androidx.datastore.rxjava2.RxDataMigration<T> rxDataMigration);
method public androidx.datastore.core.DataStore<T> build();
method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T> corruptionHandler);
- method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setFileName(android.content.Context context, String fileName);
- method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setFileProducer(java.util.concurrent.Callable<java.io.File> produceFile);
method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setIoScheduler(io.reactivex.Scheduler ioScheduler);
- method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setSerializer(androidx.datastore.core.Serializer<T> serializer);
+ }
+
+ public interface RxSharedPreferencesMigration<T> {
+ method public io.reactivex.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
+ method public default io.reactivex.Single<java.lang.Boolean> shouldMigrate(T? currentData);
+ }
+
+ public final class RxSharedPreferencesMigrationBuilder<T> {
+ ctor public RxSharedPreferencesMigrationBuilder(android.content.Context context, String sharedPreferencesName, androidx.datastore.rxjava2.RxSharedPreferencesMigration<T> rxSharedPreferencesMigration);
+ method public androidx.datastore.core.DataMigration<T> build();
+ method public androidx.datastore.rxjava2.RxSharedPreferencesMigrationBuilder<T> setDeleteEmptyPreferences(boolean deleteEmptyPreferences);
+ method public androidx.datastore.rxjava2.RxSharedPreferencesMigrationBuilder<T> setKeysToMigrate(java.lang.String... keys);
}
}
diff --git a/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt b/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt
index 42e32ea..b52a1fc 100644
--- a/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt
+++ b/datastore/datastore-rxjava2/api/public_plus_experimental_current.txt
@@ -1,20 +1,37 @@
// Signature format: 4.0
package androidx.datastore.rxjava2 {
+ public interface RxDataMigration<T> {
+ method public io.reactivex.Completable cleanUp();
+ method public io.reactivex.Single<T!> migrate(T?);
+ method public io.reactivex.Single<java.lang.Boolean!> shouldMigrate(T?);
+ }
+
public final class RxDataStore {
method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Flowable<T> data(androidx.datastore.core.DataStore<T>);
method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Single<T> updateDataAsync(androidx.datastore.core.DataStore<T>, io.reactivex.functions.Function<T,io.reactivex.Single<T>> transform);
}
public final class RxDataStoreBuilder<T> {
- ctor public RxDataStoreBuilder();
+ ctor public RxDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile, androidx.datastore.core.Serializer<T> serializer);
+ ctor public RxDataStoreBuilder(android.content.Context context, String fileName, androidx.datastore.core.Serializer<T> serializer);
method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> addDataMigration(androidx.datastore.core.DataMigration<T> dataMigration);
+ method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> addRxDataMigration(androidx.datastore.rxjava2.RxDataMigration<T> rxDataMigration);
method public androidx.datastore.core.DataStore<T> build();
method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T> corruptionHandler);
- method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setFileName(android.content.Context context, String fileName);
- method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setFileProducer(java.util.concurrent.Callable<java.io.File> produceFile);
method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setIoScheduler(io.reactivex.Scheduler ioScheduler);
- method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setSerializer(androidx.datastore.core.Serializer<T> serializer);
+ }
+
+ public interface RxSharedPreferencesMigration<T> {
+ method public io.reactivex.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
+ method public default io.reactivex.Single<java.lang.Boolean> shouldMigrate(T? currentData);
+ }
+
+ public final class RxSharedPreferencesMigrationBuilder<T> {
+ ctor public RxSharedPreferencesMigrationBuilder(android.content.Context context, String sharedPreferencesName, androidx.datastore.rxjava2.RxSharedPreferencesMigration<T> rxSharedPreferencesMigration);
+ method public androidx.datastore.core.DataMigration<T> build();
+ method public androidx.datastore.rxjava2.RxSharedPreferencesMigrationBuilder<T> setDeleteEmptyPreferences(boolean deleteEmptyPreferences);
+ method public androidx.datastore.rxjava2.RxSharedPreferencesMigrationBuilder<T> setKeysToMigrate(java.lang.String... keys);
}
}
diff --git a/datastore/datastore-rxjava2/api/restricted_current.txt b/datastore/datastore-rxjava2/api/restricted_current.txt
index 42e32ea..b52a1fc 100644
--- a/datastore/datastore-rxjava2/api/restricted_current.txt
+++ b/datastore/datastore-rxjava2/api/restricted_current.txt
@@ -1,20 +1,37 @@
// Signature format: 4.0
package androidx.datastore.rxjava2 {
+ public interface RxDataMigration<T> {
+ method public io.reactivex.Completable cleanUp();
+ method public io.reactivex.Single<T!> migrate(T?);
+ method public io.reactivex.Single<java.lang.Boolean!> shouldMigrate(T?);
+ }
+
public final class RxDataStore {
method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Flowable<T> data(androidx.datastore.core.DataStore<T>);
method @kotlinx.coroutines.ExperimentalCoroutinesApi public static <T> io.reactivex.Single<T> updateDataAsync(androidx.datastore.core.DataStore<T>, io.reactivex.functions.Function<T,io.reactivex.Single<T>> transform);
}
public final class RxDataStoreBuilder<T> {
- ctor public RxDataStoreBuilder();
+ ctor public RxDataStoreBuilder(java.util.concurrent.Callable<java.io.File> produceFile, androidx.datastore.core.Serializer<T> serializer);
+ ctor public RxDataStoreBuilder(android.content.Context context, String fileName, androidx.datastore.core.Serializer<T> serializer);
method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> addDataMigration(androidx.datastore.core.DataMigration<T> dataMigration);
+ method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> addRxDataMigration(androidx.datastore.rxjava2.RxDataMigration<T> rxDataMigration);
method public androidx.datastore.core.DataStore<T> build();
method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setCorruptionHandler(androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T> corruptionHandler);
- method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setFileName(android.content.Context context, String fileName);
- method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setFileProducer(java.util.concurrent.Callable<java.io.File> produceFile);
method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setIoScheduler(io.reactivex.Scheduler ioScheduler);
- method public androidx.datastore.rxjava2.RxDataStoreBuilder<T> setSerializer(androidx.datastore.core.Serializer<T> serializer);
+ }
+
+ public interface RxSharedPreferencesMigration<T> {
+ method public io.reactivex.Single<T> migrate(androidx.datastore.migrations.SharedPreferencesView sharedPreferencesView, T? currentData);
+ method public default io.reactivex.Single<java.lang.Boolean> shouldMigrate(T? currentData);
+ }
+
+ public final class RxSharedPreferencesMigrationBuilder<T> {
+ ctor public RxSharedPreferencesMigrationBuilder(android.content.Context context, String sharedPreferencesName, androidx.datastore.rxjava2.RxSharedPreferencesMigration<T> rxSharedPreferencesMigration);
+ method public androidx.datastore.core.DataMigration<T> build();
+ method public androidx.datastore.rxjava2.RxSharedPreferencesMigrationBuilder<T> setDeleteEmptyPreferences(boolean deleteEmptyPreferences);
+ method public androidx.datastore.rxjava2.RxSharedPreferencesMigrationBuilder<T> setKeysToMigrate(java.lang.String... keys);
}
}
diff --git a/datastore/datastore-rxjava2/build.gradle b/datastore/datastore-rxjava2/build.gradle
index 8bc10fe..83003c2 100644
--- a/datastore/datastore-rxjava2/build.gradle
+++ b/datastore/datastore-rxjava2/build.gradle
@@ -27,18 +27,12 @@
}
android {
- buildTypes{
- debug {
- multiDexEnabled = true
- }
- }
-
sourceSets {
test.java.srcDirs += 'src/test-common/java'
+ androidTest.java.srcDirs += 'src/test-common/java'
}
}
-
dependencies {
api(KOTLIN_STDLIB)
api(KOTLIN_COROUTINES_CORE)
@@ -53,6 +47,11 @@
testImplementation(KOTLIN_COROUTINES_TEST)
testImplementation(TRUTH)
testImplementation(project(":internal-testutils-truth"))
+
+ androidTestImplementation(JUNIT)
+ androidTestImplementation(project(":internal-testutils-truth"))
+ androidTestImplementation(ANDROIDX_TEST_RUNNER)
+ androidTestImplementation(ANDROIDX_TEST_CORE)
}
androidx {
@@ -63,10 +62,3 @@
description = "Android DataStore Core - contains wrappers for using DataStore using RxJava2"
legacyDisableKotlinStrictApiMode = true
}
-
-// Allow usage of Kotlin's @OptIn.
-tasks.withType(KotlinCompile).configureEach {
- kotlinOptions {
- freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
- }
-}
\ No newline at end of file
diff --git a/car/app/app/src/androidTest/AndroidManifest.xml b/datastore/datastore-rxjava2/src/androidTest/AndroidManifest.xml
similarity index 94%
rename from car/app/app/src/androidTest/AndroidManifest.xml
rename to datastore/datastore-rxjava2/src/androidTest/AndroidManifest.xml
index 3bc2684..bf9d5c2 100644
--- a/car/app/app/src/androidTest/AndroidManifest.xml
+++ b/datastore/datastore-rxjava2/src/androidTest/AndroidManifest.xml
@@ -15,5 +15,6 @@
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="androidx.car.app">
+ package="androidx.datastore.rxjava2">
+
</manifest>
diff --git a/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/AssertThrows.kt b/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/AssertThrows.kt
new file mode 100644
index 0000000..cc1f3493
--- /dev/null
+++ b/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/AssertThrows.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.datastore.rxjava2
+
+@Suppress("UNCHECKED_CAST")
+internal fun <T : Throwable?> assertThrows(
+ expectedType: Class<T>,
+ runnable: Runnable
+): T {
+ try {
+ runnable.run()
+ } catch (t: Throwable) {
+ if (!expectedType.isInstance(t)) {
+ throw RuntimeException(t)
+ }
+ return t as T
+ }
+ throw AssertionError(
+ String.format(
+ "Expected %s wasn't thrown",
+ expectedType.simpleName
+ )
+ )
+}
diff --git a/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/RxDataStoreBuilderTest.java b/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/RxDataStoreBuilderTest.java
new file mode 100644
index 0000000..2d3a0ae
--- /dev/null
+++ b/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/RxDataStoreBuilderTest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.datastore.rxjava2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.datastore.core.DataStore;
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+import io.reactivex.Completable;
+import io.reactivex.Scheduler;
+import io.reactivex.Single;
+import io.reactivex.schedulers.Schedulers;
+
+public class RxDataStoreBuilderTest {
+ @Rule
+ public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ private static Single<Byte> incrementByte(Byte byteIn) {
+ return Single.just(++byteIn);
+ }
+
+ @Test
+ public void testConstructWithProduceFile() throws Exception {
+ File file = tempFolder.newFile();
+ DataStore<Byte> dataStore =
+ new RxDataStoreBuilder<Byte>(() -> file, new TestingSerializer())
+ .build();
+ Single<Byte> incrementByte = RxDataStore.updateDataAsync(dataStore,
+ RxDataStoreBuilderTest::incrementByte);
+ assertThat(incrementByte.blockingGet()).isEqualTo(1);
+ // Construct it again and confirm that the data is still there:
+ dataStore =
+ new RxDataStoreBuilder<Byte>(() -> file, new TestingSerializer())
+ .build();
+ assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(1);
+ }
+
+ @Test
+ public void testConstructWithContextAndName() throws Exception {
+ Context context = ApplicationProvider.getApplicationContext();
+ String name = "my_data_store";
+ DataStore<Byte> dataStore =
+ new RxDataStoreBuilder<Byte>(context, name, new TestingSerializer())
+ .build();
+ Single<Byte> set1 = RxDataStore.updateDataAsync(dataStore, input -> Single.just((byte) 1));
+ assertThat(set1.blockingGet()).isEqualTo(1);
+ // Construct it again and confirm that the data is still there:
+ dataStore =
+ new RxDataStoreBuilder<Byte>(context, name, new TestingSerializer())
+ .build();
+ assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(1);
+ // Construct it again with the expected file path and confirm that the data is there:
+ dataStore =
+ new RxDataStoreBuilder<Byte>(() -> new File(context.getFilesDir().getPath()
+ + "/datastore/" + name), new TestingSerializer()
+ )
+ .build();
+ assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(1);
+ }
+
+ @Test
+ public void testMigrationsAreInstalledAndRun() throws Exception {
+ RxDataMigration<Byte> plusOneMigration = new RxDataMigration<Byte>() {
+ @NonNull
+ @Override
+ public Single<Boolean> shouldMigrate(@NonNull Byte currentData) {
+ return Single.just(true);
+ }
+
+ @NonNull
+ @Override
+ public Single<Byte> migrate(@NonNull Byte currentData) {
+ return incrementByte(currentData);
+ }
+
+ @NonNull
+ @Override
+ public Completable cleanUp() {
+ return Completable.complete();
+ }
+ };
+
+ DataStore<Byte> dataStore = new RxDataStoreBuilder<Byte>(
+ () -> tempFolder.newFile(), new TestingSerializer())
+ .addRxDataMigration(plusOneMigration)
+ .build();
+
+ assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(1);
+ }
+
+ @Test
+ public void testSpecifiedSchedulerIsUser() throws Exception {
+ Scheduler singleThreadedScheduler =
+ Schedulers.from(Executors.newSingleThreadExecutor(new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable r) {
+ return new Thread(r, "TestingThread");
+ }
+ }));
+
+
+ DataStore<Byte> dataStore = new RxDataStoreBuilder<Byte>(() -> tempFolder.newFile(),
+ new TestingSerializer())
+ .setIoScheduler(singleThreadedScheduler)
+ .build();
+ Single<Byte> update = RxDataStore.updateDataAsync(dataStore, input -> {
+ Thread currentThread = Thread.currentThread();
+ assertThat(currentThread.getName()).isEqualTo("TestingThread");
+ return Single.just(input);
+ });
+ assertThat(update.blockingGet()).isEqualTo((byte) 0);
+ Single<Byte> subsequentUpdate = RxDataStore.updateDataAsync(dataStore, input -> {
+ Thread currentThread = Thread.currentThread();
+ assertThat(currentThread.getName()).isEqualTo("TestingThread");
+ return Single.just(input);
+ });
+ assertThat(subsequentUpdate.blockingGet()).isEqualTo((byte) 0);
+ }
+
+ @Test
+ public void testCorruptionHandlerIsUser() {
+ TestingSerializer testingSerializer = new TestingSerializer();
+ testingSerializer.setFailReadWithCorruptionException(true);
+ ReplaceFileCorruptionHandler<Byte> replaceFileCorruptionHandler =
+ new ReplaceFileCorruptionHandler<Byte>(exception -> (byte) 99);
+
+
+ DataStore<Byte> dataStore = new RxDataStoreBuilder<Byte>(
+ () -> tempFolder.newFile(),
+ testingSerializer)
+ .setCorruptionHandler(replaceFileCorruptionHandler)
+ .build();
+ assertThat(RxDataStore.data(dataStore).blockingFirst()).isEqualTo(99);
+ }
+}
diff --git a/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/RxSharedPreferencesMigrationTest.java b/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/RxSharedPreferencesMigrationTest.java
new file mode 100644
index 0000000..c8e0b79
--- /dev/null
+++ b/datastore/datastore-rxjava2/src/androidTest/java/androidx/datastore/rxjava2/RxSharedPreferencesMigrationTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.rxjava2;
+
+import static androidx.testutils.AssertionsKt.assertThrows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import androidx.datastore.core.DataMigration;
+import androidx.datastore.core.DataStore;
+import androidx.datastore.migrations.SharedPreferencesView;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+
+import io.reactivex.Single;
+
+public class RxSharedPreferencesMigrationTest {
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ private final String mSharedPrefsName = "shared_prefs_name";
+
+
+ private Context mContext;
+ private SharedPreferences mSharedPrefs;
+ private File mDatastoreFile;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = ApplicationProvider.getApplicationContext();
+ mSharedPrefs = mContext.getSharedPreferences(mSharedPrefsName, Context.MODE_PRIVATE);
+ mDatastoreFile = temporaryFolder.newFile("test_file.preferences_pb");
+
+ assertThat(mSharedPrefs.edit().clear().commit()).isTrue();
+ }
+
+ @Test
+ public void testShouldMigrateSkipsMigration() {
+ RxSharedPreferencesMigration<Byte> skippedMigration =
+ new RxSharedPreferencesMigration<Byte>() {
+ @NotNull
+ @Override
+ public Single<Boolean> shouldMigrate(Byte currentData) {
+ return Single.just(false);
+ }
+
+ @NotNull
+ @Override
+ public Single<Byte> migrate(
+ @NotNull SharedPreferencesView sharedPreferencesView,
+ Byte currentData) {
+ return Single.error(
+ new IllegalStateException("We shouldn't reach this point!"));
+ }
+ };
+
+
+ DataMigration<Byte> spMigration =
+ getSpMigrationBuilder(skippedMigration).build();
+
+ DataStore<Byte> dataStoreWithMigrations = getDataStoreWithMigration(spMigration);
+
+ assertThat(RxDataStore.data(dataStoreWithMigrations).blockingFirst()).isEqualTo(0);
+ }
+
+ @Test
+ public void testSharedPrefsViewContainsSpecifiedKeys() {
+ String includedKey = "key1";
+ int includedVal = 99;
+ String notMigratedKey = "key2";
+
+ assertThat(mSharedPrefs.edit().putInt(includedKey, includedVal).putInt(notMigratedKey,
+ 123).commit()).isTrue();
+
+ DataMigration<Byte> dataMigration =
+ getSpMigrationBuilder(
+ new DefaultMigration() {
+ @NotNull
+ @Override
+ public Single<Byte> migrate(
+ @NotNull SharedPreferencesView sharedPreferencesView,
+ Byte currentData) {
+ assertThat(sharedPreferencesView.contains(includedKey)).isTrue();
+ assertThat(sharedPreferencesView.getAll().size()).isEqualTo(1);
+ assertThrows(IllegalStateException.class,
+ () -> sharedPreferencesView.getInt(notMigratedKey, -1));
+
+ return Single.just((byte) 50);
+ }
+ }
+ ).setKeysToMigrate(includedKey).build();
+
+ DataStore<Byte> byteStore = getDataStoreWithMigration(dataMigration);
+
+ assertThat(RxDataStore.data(byteStore).blockingFirst()).isEqualTo(50);
+
+ assertThat(mSharedPrefs.contains(includedKey)).isFalse();
+ assertThat(mSharedPrefs.contains(notMigratedKey)).isTrue();
+ }
+
+
+ @Test
+ public void testSharedPrefsViewWithAllKeysSpecified() {
+ String includedKey = "key1";
+ String includedKey2 = "key2";
+ int value = 99;
+
+ assertThat(mSharedPrefs.edit().putInt(includedKey, value).putInt(includedKey2,
+ value).commit()).isTrue();
+
+ DataMigration<Byte> dataMigration =
+ getSpMigrationBuilder(
+ new DefaultMigration() {
+ @NotNull
+ @Override
+ public Single<Byte> migrate(
+ @NotNull SharedPreferencesView sharedPreferencesView,
+ Byte currentData) {
+ assertThat(sharedPreferencesView.contains(includedKey)).isTrue();
+ assertThat(sharedPreferencesView.contains(includedKey2)).isTrue();
+ assertThat(sharedPreferencesView.getAll().size()).isEqualTo(2);
+
+ return Single.just((byte) 50);
+ }
+ }
+ ).build();
+
+ DataStore<Byte> byteStore = getDataStoreWithMigration(dataMigration);
+
+ assertThat(RxDataStore.data(byteStore).blockingFirst()).isEqualTo(50);
+
+ assertThat(mSharedPrefs.contains(includedKey)).isFalse();
+ assertThat(mSharedPrefs.contains(includedKey2)).isFalse();
+ }
+
+ @Test
+ public void testDeletesEmptySharedPreferences() {
+ String key = "key";
+ String value = "value";
+ assertThat(mSharedPrefs.edit().putString(key, value).commit()).isTrue();
+
+ DataMigration<Byte> dataMigration =
+ getSpMigrationBuilder(new DefaultMigration()).setDeleteEmptyPreferences(
+ true).build();
+ DataStore<Byte> byteStore = getDataStoreWithMigration(dataMigration);
+ assertThat(RxDataStore.data(byteStore).blockingFirst()).isEqualTo(0);
+
+ // Check that the shared preferences files are deleted
+ File prefsDir = new File(mContext.getApplicationInfo().dataDir, "shared_prefs");
+ File prefsFile = new File(prefsDir, mSharedPrefsName + ".xml");
+ File backupPrefsFile = new File(prefsFile.getPath() + ".bak");
+ assertThat(prefsFile.exists()).isFalse();
+ assertThat(backupPrefsFile.exists()).isFalse();
+ }
+
+ private RxSharedPreferencesMigrationBuilder<Byte> getSpMigrationBuilder(
+ RxSharedPreferencesMigration<Byte> rxSharedPreferencesMigration) {
+ return new RxSharedPreferencesMigrationBuilder<Byte>(mContext, mSharedPrefsName,
+ rxSharedPreferencesMigration);
+ }
+
+ private DataStore<Byte> getDataStoreWithMigration(DataMigration<Byte> dataMigration) {
+ return new RxDataStoreBuilder<Byte>(() -> mDatastoreFile, new TestingSerializer())
+ .addDataMigration(dataMigration).build();
+ }
+
+
+ private static class DefaultMigration implements RxSharedPreferencesMigration<Byte> {
+
+ @NotNull
+ @Override
+ public Single<Boolean> shouldMigrate(Byte currentData) {
+ return Single.just(true);
+ }
+
+ @NotNull
+ @Override
+ public Single<Byte> migrate(@NotNull SharedPreferencesView sharedPreferencesView,
+ Byte currentData) {
+ return Single.just(currentData);
+ }
+ }
+}
diff --git a/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxDataMigration.java b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxDataMigration.java
new file mode 100644
index 0000000..79df6a1
--- /dev/null
+++ b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxDataMigration.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.rxjava2;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import io.reactivex.Completable;
+import io.reactivex.Single;
+
+/**
+ * Interface for migrations to DataStore. Methods on this migration ([shouldMigrate], [migrate]
+ * and [cleanUp]) may be called multiple times, so their implementations must be idempotent.
+ * These methods may be called multiple times if DataStore encounters issues when writing the
+ * newly migrated data to disk or if any migration installed in the same DataStore throws an
+ * Exception.
+ *
+ * If you're migrating from SharedPreferences see [SharedPreferencesMigration].
+ *
+ * @param <T> the exception type
+ */
+public interface RxDataMigration<T> {
+
+ /**
+ * Return whether this migration needs to be performed. If this returns false, no migration or
+ * cleanup will occur. Apps should do the cheapest possible check to determine if this migration
+ * should run, since this will be called every time the DataStore is initialized. This method
+ * may be run multiple times when any failure is encountered.
+ *
+ * Note that this will always be called before each call to [migrate].
+ *
+ * @param currentData the current data (which might already populated from previous runs of this
+ * or other migrations). Only Nullable if the type used with DataStore is
+ * Nullable.
+ */
+ @NonNull
+ Single<Boolean> shouldMigrate(@Nullable T currentData);
+
+ /**
+ * Perform the migration. Implementations should be idempotent since this may be called
+ * multiple times. If migrate fails, DataStore will not commit any data to disk, cleanUp will
+ * not be called, and the exception will be propagated back to the DataStore call that
+ * triggered the migration. Future calls to DataStore will result in DataMigrations being
+ * attempted again. This method may be run multiple times when any failure is encountered.
+ *
+ * Note that this will always be called before a call to [cleanUp].
+ *
+ * @param currentData the current data (it might be populated from other migrations or from
+ * manual changes before this migration was added to the app). Only
+ * Nullable if the type used with DataStore is Nullable.
+ * @return The migrated data.
+ */
+ @NonNull
+ Single<T> migrate(@Nullable T currentData);
+
+ /**
+ * Clean up any old state/data that was migrated into the DataStore. This will not be called
+ * if the migration fails. If cleanUp throws an exception, the exception will be propagated
+ * back to the DataStore call that triggered the migration and future calls to DataStore will
+ * result in DataMigrations being attempted again. This method may be run multiple times when
+ * any failure is encountered.
+ */
+ @NonNull
+ Completable cleanUp();
+}
diff --git a/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxDataStoreBuilder.kt b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxDataStoreBuilder.kt
index 1685286..070d0a1c 100644
--- a/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxDataStoreBuilder.kt
+++ b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxDataStoreBuilder.kt
@@ -28,6 +28,7 @@
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.rx2.asCoroutineDispatcher
+import kotlinx.coroutines.rx2.await
import java.io.File
import java.util.concurrent.Callable
@@ -35,15 +36,49 @@
* RxSharedPreferencesMigrationBuilder class for a DataStore that works on a single process.
*/
@SuppressLint("TopLevelBuilder")
-public class RxDataStoreBuilder<T>() {
+public class RxDataStoreBuilder<T> {
- // Either produceFile or context & name must be set, but not both.
+ /**
+ * Create a RxDataStoreBuilder with the callable which returns the File that DataStore acts on.
+ * The user is responsible for ensuring that there is never more than one DataStore acting on
+ * a file at a time.
+ *
+ * @param produceFile Function which returns the file that the new DataStore will act on. The
+ * function must return the same path every time. No two instances of DataStore should act on
+ * the same file at the same time.
+ * @param serializer the serializer for the type that this DataStore acts on.
+ */
+ public constructor(produceFile: Callable<File>, serializer: Serializer<T>) {
+ this.produceFile = produceFile
+ this.serializer = serializer
+ }
+
+ /**
+ * Create a RxDataStoreBuilder with the Context and name from which to derive the DataStore
+ * file. The file is generated by See [Context.createDataStore] for more info. The user is
+ * responsible for ensuring that there is never more than one DataStore acting on a file at a
+ * time.
+ *
+ * @param context the context from which we retrieve files directory.
+ * @param fileName the filename relative to Context.filesDir that DataStore acts on. The File is
+ * obtained by calling File(context.filesDir, fileName). No two instances of DataStore should
+ * act on the same file at the same time.
+ * @param serializer the serializer for the type that this DataStore acts on.
+ */
+ public constructor(context: Context, fileName: String, serializer: Serializer<T>) {
+ this.context = context
+ this.name = fileName
+ this.serializer = serializer
+ }
+
+ // Either produceFile or context & name must be set, but not both. This is enforced by the
+ // two constructors.
private var produceFile: Callable<File>? = null
private var context: Context? = null
private var name: String? = null
- // Required
+ // Required. This is enforced by the constructors.
private var serializer: Serializer<T>? = null
// Optional
@@ -52,61 +87,6 @@
private val dataMigrations: MutableList<DataMigration<T>> = mutableListOf()
/**
- * Set the callable which returns the File that DataStore acts on. The user is responsible for
- * ensuring that there is never more than one DataStore acting on a file at a time.
- *
- * It is required to call either this method or [setFileName] before calling [build].
- *
- *
- * @param produceFile Function which returns the file that the new DataStore will act on. The
- * function must return the same path every time. No two instances of DataStore should act on
- * the same file at the same time.
- * @throws IllegalStateException if context and name are already set
- * @return this
- */
- @Suppress("MissingGetterMatchingBuilder")
- public fun setFileProducer(produceFile: Callable<File>): RxDataStoreBuilder<T> = apply {
- check(context == null) { "Only call setFileProducer or setContextAndName" }
- check(name == null) { "Only call setFileProducer or setContextAndName" }
- this.produceFile = produceFile
- }
-
- /**
- * Set the Context and name from which to derive the DataStore file. The file is generated by
- * See [Context.createDataStore] for more info. The user is responsible for ensuring that
- * there is never more than one DataStore acting on a file at a time.
- *
- * It is required to call either this method or [setFileProducer] before calling [build].
- *
- * @param context the context from which we retrieve files directory.
- * @param fileName the filename relative to Context.filesDir that DataStore acts on. The File is
- * obtained by calling File(context.filesDir, fileName). No two instances of DataStore should
- * act on the same file at the same time.
- * @throws IllegalStateException if produceFile is already set
- * @return this
- */
- @Suppress("MissingGetterMatchingBuilder")
- public fun setFileName(context: Context, fileName: String): RxDataStoreBuilder<T> =
- apply {
- check(produceFile == null) { "Only call setFileProducer or setContextAndName" }
- this.context = context
- this.name = fileName
- }
-
- /**
- * Set the serializer that this DataStore acts on.
- *
- * This parameter is required.
- *
- * @param serializer the serializer for the type that this DataStore acts on.
- * @return this
- */
- @Suppress("MissingGetterMatchingBuilder")
- public fun setSerializer(serializer: Serializer<T>): RxDataStoreBuilder<T> = apply {
- this.serializer = serializer
- }
-
- /**
* Set the Scheduler on which to perform IO and transform operations. This is converted into
* a CoroutineDispatcher before being added to DataStore.
*
@@ -132,6 +112,18 @@
RxDataStoreBuilder<T> = apply { this.corruptionHandler = corruptionHandler }
/**
+ * Add an RxDataMigration to the DataStore. Migrations are run in the order they are added.
+ *
+ * @param rxDataMigration the migration to add.
+ * @return this
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ public fun addRxDataMigration(rxDataMigration: RxDataMigration<T>): RxDataStoreBuilder<T> =
+ apply {
+ this.dataMigrations.add(DataMigrationFromRxDataMigration(rxDataMigration))
+ }
+
+ /**
* Add a DataMigration to the Datastore. Migrations are run in the order they are added.
*
* @param dataMigration the migration to add
@@ -145,15 +137,9 @@
/**
* Build the DataStore.
*
- * @throws IllegalStateException if serializer is not set or if neither produceFile not
- * context and name are set.
* @return the DataStore with the provided parameters
*/
public fun build(): DataStore<T> {
- check(serializer != null) {
- "Serializer must be set."
- }
-
val scope = CoroutineScope(ioScheduler.asCoroutineDispatcher())
return if (produceFile != null) {
@@ -175,7 +161,24 @@
migrations = dataMigrations
)
} else {
- throw IllegalStateException("Either produceFile or context and name must be set.")
+ error(
+ "Either produceFile or context and name must be set. This should never happen."
+ )
}
}
}
+
+internal class DataMigrationFromRxDataMigration<T>(private val migration: RxDataMigration<T>) :
+ DataMigration<T> {
+ override suspend fun shouldMigrate(currentData: T): Boolean {
+ return migration.shouldMigrate(currentData).await()
+ }
+
+ override suspend fun migrate(currentData: T): T {
+ return migration.migrate(currentData).await()
+ }
+
+ override suspend fun cleanUp() {
+ migration.cleanUp().await()
+ }
+}
diff --git a/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt
new file mode 100644
index 0000000..e84e377
--- /dev/null
+++ b/datastore/datastore-rxjava2/src/main/java/androidx/datastore/rxjava2/RxSharedPreferencesMigration.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.rxjava2
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.datastore.core.DataMigration
+import androidx.datastore.migrations.SharedPreferencesMigration
+import androidx.datastore.migrations.SharedPreferencesView
+import io.reactivex.Single
+import kotlinx.coroutines.rx2.await
+
+/**
+ * Client implemented migration interface.
+ **/
+public interface RxSharedPreferencesMigration<T> {
+ /**
+ * Whether or not the migration should be run. This can be used to skip a read from the
+ * SharedPreferences.
+ *
+ * @param currentData the most recently persisted data
+ * @return a Single indicating whether or not the migration should be run.
+ */
+ public fun shouldMigrate(currentData: T): Single<Boolean> {
+ return Single.just(true)
+ }
+
+ /**
+ * Maps SharedPreferences into T. Implementations should be idempotent
+ * since this may be called multiple times. See [DataMigration.migrate] for more
+ * information. The method accepts a SharedPreferencesView which is the view of the
+ * SharedPreferences to migrate from (limited to [keysToMigrate] and a T which represent
+ * the current data. The function must return the migrated data.
+ *
+ * @param sharedPreferencesView the current state of the SharedPreferences
+ * @param currentData the most recently persisted data
+ * @return a Single of the updated data
+ */
+ public fun migrate(sharedPreferencesView: SharedPreferencesView, currentData: T): Single<T>
+}
+
+/**
+ * RxSharedPreferencesMigrationBuilder for the RxSharedPreferencesMigration.
+ */
+@SuppressLint("TopLevelBuilder")
+public class RxSharedPreferencesMigrationBuilder<T>
+/**
+ * Construct a RxSharedPreferencesMigrationBuilder.
+ *
+ * @param context the Context used for getting the SharedPreferences.
+ * @param sharedPreferencesName the name of the SharedPreference from which to migrate.
+ * @param rxSharedPreferencesMigration the user implemented migration for this SharedPreference.
+ */
+constructor(
+ private val context: Context,
+ private val sharedPreferencesName: String,
+ private val rxSharedPreferencesMigration: RxSharedPreferencesMigration<T>
+) {
+
+ /** Optional */
+ private var deleteEmptyPreference: Boolean = true
+ private var keysToMigrate: Set<String>? = null
+
+ /**
+ * Set the list of keys to migrate. The keys will be mapped to datastore.Preferences with
+ * their same values. If the key is already present in the new Preferences, the key
+ * will not be migrated again. If the key is not present in the SharedPreferences it
+ * will not be migrated.
+ *
+ * This method is optional and if keysToMigrate is not set, all keys will be migrated from the
+ * existing SharedPreferences.
+ *
+ * @param keys the keys to migrate
+ * @return this
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ public fun setKeysToMigrate(vararg keys: String):
+ RxSharedPreferencesMigrationBuilder<T> = apply {
+ keysToMigrate = setOf(*keys)
+ }
+
+ /**
+ * If enabled and the SharedPreferences are empty (i.e. no remaining
+ * keys) after this migration runs, the leftover SharedPreferences file is deleted. Note that
+ * this cleanup runs only if the migration itself runs, i.e., if the keys were never in
+ * SharedPreferences to begin with then the (potentially) empty SharedPreferences
+ * won't be cleaned up by this option. This functionality is best effort - if there
+ * is an issue deleting the SharedPreferences file it will be silently ignored.
+ *
+ * This method is optional and defaults to true.
+ *
+ * @param deleteEmptyPreferences whether or not to delete the empty shared preferences file
+ * @return this
+ */
+ @Suppress("MissingGetterMatchingBuilder")
+ public fun setDeleteEmptyPreferences(deleteEmptyPreferences: Boolean):
+ RxSharedPreferencesMigrationBuilder<T> = apply {
+ this.deleteEmptyPreference = deleteEmptyPreferences
+ }
+
+ public fun build(): DataMigration<T> {
+ return SharedPreferencesMigration(
+ context = context,
+ sharedPreferencesName = sharedPreferencesName,
+ migrate = { spView, curData ->
+ rxSharedPreferencesMigration.migrate(spView, curData).await()
+ },
+ keysToMigrate = keysToMigrate,
+ deleteEmptyPreferences = deleteEmptyPreference,
+ shouldRunMigration = { curData ->
+ rxSharedPreferencesMigration.shouldMigrate(curData).await()
+ }
+ )
+ }
+}
diff --git a/development/build_log_processor.sh b/development/build_log_processor.sh
index 9802bf4..15a6082 100755
--- a/development/build_log_processor.sh
+++ b/development/build_log_processor.sh
@@ -86,8 +86,6 @@
if $SCRIPT_PATH/build_log_simplifier.py --validate $logFile >&2; then
echo No unrecognized messages found in build log
else
- echo >&2
- echo "Build log validation, enabled by the argument $validateArgument, failed" >&2
exit 1
fi
fi
diff --git a/development/build_log_simplifier/build_log_simplifier.py b/development/build_log_simplifier/build_log_simplifier.py
index 34d6e9d..83f30adc 100755
--- a/development/build_log_simplifier/build_log_simplifier.py
+++ b/development/build_log_simplifier/build_log_simplifier.py
@@ -146,46 +146,6 @@
prev_line_is_boring = False
return result
-def remove_known_uninteresting_lines(lines):
- skipLines = {
- "A fine-grained performance profile is available: use the --scan option.",
- "* Get more help at https://help.gradle.org",
- "Use '--warning-mode all' to show the individual deprecation warnings.",
- "See https://docs.gradle.org/6.5/userguide/command_line_interface.html#sec:command_line_warnings",
-
- "Note: Some input files use or override a deprecated API.",
- "Note: Recompile with -Xlint:deprecation for details.",
- "Note: Some input files use unchecked or unsafe operations.",
- "Note: Recompile with -Xlint:unchecked for details.",
-
- "w: ATTENTION!",
- "This build uses unsafe internal compiler arguments:",
- "-XXLanguage:-NewInference",
- "-XXLanguage:+InlineClasses",
- "This mode is not recommended for production use,",
- "as no stability/compatibility guarantees are given on",
- "compiler or generated code. Use it at your own risk!"
- }
- skipPrefixes = [
- "See the profiling report at:",
-
- "Deprecated Gradle features were used in this build"
- ]
- result = []
- for line in lines:
- stripped = line.strip()
- if stripped in skipLines:
- continue
- include = True
- for prefix in skipPrefixes:
- if stripped.startswith(prefix):
- include = False
- break
- if include:
- result.append(line)
- return result
-
-
# Returns the path of the config file holding exemptions for deterministic/consistent output.
# These exemptions can be garbage collected via the `--gc` argument
def get_deterministic_exemptions_path():
@@ -260,6 +220,23 @@
prev_blank = False
return result
+def extract_task_name(line):
+ prefix = "> Task "
+ if line.startswith(prefix):
+ return line[len(prefix):].strip()
+ return None
+
+def is_task_line(line):
+ return extract_task_name(line) is not None
+
+def extract_task_names(lines):
+ names = []
+ for line in lines:
+ name = extract_task_name(line)
+ if name is not None and name not in names:
+ names.append(name)
+ return names
+
# If a task has no output (or only blank output), this function removes the task (and its output)
# For example, turns this:
# > Task :a
@@ -277,8 +254,8 @@
pending_task = None
pending_blanks = []
for line in lines:
- is_task = line.startswith("> Task ") or line.startswith("> Configure project ")
- if is_task:
+ is_section = is_task_line(line) or line.startswith("> Configure project ")
+ if is_section:
pending_task = line
pending_blanks = []
elif line.strip() == "":
@@ -408,12 +385,12 @@
if line == "":
continue
# save task name
- is_task = False
- if line.startswith("> Task :") or line.startswith("> Configure project "):
+ is_section = False
+ if is_task_line(line) or line.startswith("> Configure project "):
# If a task creates output, we record its name
line = "# " + line
pending_task_line = line
- is_task = True
+ is_section = True
# determine where to put task name
current_found_index = existing_matcher.index_first_matching_regex(line)
if current_found_index is not None:
@@ -423,7 +400,7 @@
pending_task_line = None
continue
# skip outputting task names for tasks that don't output anything
- if is_task:
+ if is_section:
continue
# escape message
@@ -543,7 +520,6 @@
if not validate:
interesting_lines = select_failing_task_output(interesting_lines)
interesting_lines = shorten_uninteresting_stack_frames(interesting_lines)
- interesting_lines = remove_known_uninteresting_lines(interesting_lines)
interesting_lines = remove_by_regexes(interesting_lines, exemption_regexes, validate)
interesting_lines = collapse_tasks_having_no_output(interesting_lines)
interesting_lines = collapse_consecutive_blank_lines(interesting_lines)
@@ -562,28 +538,29 @@
if len(interesting_lines) != 0:
print("")
print("=" * 80)
- print("build_log_simplifier.py: Error: Found " + str(len(interesting_lines)) + " lines of new warning output:")
+ print("build_log_simplifier.py: Error: Found " + str(len(interesting_lines)) + " new lines of warning output!")
print("")
- print("".join(interesting_lines))
- print("=" * 80)
- print("Error: build_log_simplifier.py found " + str(len(interesting_lines)) + " new lines of output")
+ print("The new output:")
+ print(" " + " ".join(interesting_lines))
print("")
- print(" Log : " + ",".join(log_paths))
- print(" Baseline: " + get_deterministic_exemptions_path())
+ print("To reproduce this failure:")
+ print(" Try $ ./gradlew -Pandroidx.validateNoUnrecognizedMessages --rerun-tasks " + " ".join(extract_task_names(interesting_lines)))
+ print("")
+ print("Instructions:")
+ print(" Fix these messages if you can.")
+ print(" Otherwise, you may suppress them.")
+ print(" See also https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/development/build_log_simplifier/VALIDATION_FAILURE.md")
+ print("")
new_exemptions_path = log_paths[0] + ".ignore"
# filter out any inconsistently observed messages so we don't try to exempt them twice
all_lines = remove_by_regexes(all_lines, flake_exemption_regexes, validate)
# update deterministic exemptions file based on the result
suggested = generate_suggested_exemptions(all_lines, deterministic_exemption_regexes, arguments.gc)
writelines(new_exemptions_path, suggested)
- print("")
- print("Please fix or suppress these new messages in the tool that generates them.")
- print("If you cannot, then you can exempt them by doing:")
- print("")
- print(" cp " + new_exemptions_path + " " + get_deterministic_exemptions_path())
- print("")
- print("For more information, see https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/development/build_log_simplifier/VALIDATION_FAILURE.md")
- print("=" * 80)
+ print("Files:")
+ print(" Full Log : " + ",".join(log_paths))
+ print(" Baseline : " + get_deterministic_exemptions_path())
+ print(" Autogenerated new baseline : " + new_exemptions_path)
exit(1)
else:
print("".join(interesting_lines))
diff --git a/development/build_log_simplifier/message-flakes.ignore b/development/build_log_simplifier/message-flakes.ignore
index 798e21b..eb09b8e 100644
--- a/development/build_log_simplifier/message-flakes.ignore
+++ b/development/build_log_simplifier/message-flakes.ignore
@@ -15,4 +15,4 @@
Stream closed
# > 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]+ tests .*failed.*
+[0-9]+ test.*failed.*
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 4306a57..c35170b 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -400,6 +400,8 @@
Html results of .* zipped into.*\.zip
# > Task :annotation:annotation-experimental-lint:test
WARNING: An illegal reflective access operation has occurred
+WARNING\: Illegal reflective access by org\.jetbrains\.kotlin\.com\.intellij\.util\.ReflectionUtil \(file\:\$CHECKOUT\/prebuilts\/androidx\/external\/org\/jetbrains\/kotlin\/kotlin\-compiler\-embeddable\/[0-9]+\.[0-9]+\.[0-9]+\/kotlin\-compiler\-embeddable\-[0-9]+\.[0-9]+\.[0-9]+\.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/[0-9]+\.[0-9]+\.[0-9]+/kotlin\-compiler\-[0-9]+\.[0-9]+\.[0-9]+\.jar\) to method java\.util\.ResourceBundle\.setParent\(java\.util\.ResourceBundle\)
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[0-9]+\.jar\) to field com\.sun\.tools\.javac\.code\.Symbol\.owner
WARNING: Illegal reflective access by com\.intellij\.util\.ReflectionUtil \(file:\$CHECKOUT/prebuilts/androidx/external/org/jetbrains/kotlin/kotlin\-compiler/[0-9]+\.[0-9]+\.[0-9]+\-M[0-9]+/kotlin\-compiler\-[0-9]+\.[0-9]+\.[0-9]+\-M[0-9]+\.jar\) to method java\.util\.ResourceBundle\.setParent\(java\.util\.ResourceBundle\)
@@ -408,13 +410,10 @@
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
-[0-9]+ tests completed, [0-9]+ failed, [0-9]+ skipped
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
-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\.
# > Task :enterprise-feedback-testing:compileDebugUnitTestJavaWithJavac
Note: \$SUPPORT/enterprise/feedback/testing/src/test/java/androidx/enterprise/feedback/FakeKeyedAppStatesReporterTest\.java uses or overrides a deprecated API\.
# > Task :biometric:biometric:compileDebugUnitTestJavaWithJavac
@@ -446,9 +445,6 @@
WARNING: Please consider reporting this to the maintainers of androidx\.room\.compiler\.processing\.javac\.JavacProcessingEnvMessager\$Companion\$isFromCompiledClass\$[0-9]+
# > Task :wear:wear-watchface-complications-rendering:compileDebugUnitTestJavaWithJavac
Note\: \$SUPPORT\/wear\/wear\-watchface\-complications\-rendering\/src\/test\/java\/androidx\/wear\/watchface\/complications\/rendering\/RoundedDrawableTest\.java uses or overrides a deprecated API\.
-# > 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
@@ -467,7 +463,6 @@
# > Task :annotation:annotation-experimental-lint:compileKotlin
w\: ATTENTION\!
This build uses unsafe internal compiler arguments\:
-\-XXLanguage\:\-NewInference
This mode is not recommended for production use\,
as no stability\/compatibility guarantees are given on
compiler or generated code\. Use it at your own risk\!
@@ -610,7 +605,6 @@
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
-[0-9]+ test completed, [0-9]+ failed
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/manifestMerger[0-9]+\.xml Warning:
diff --git a/development/build_log_simplifier/test.py b/development/build_log_simplifier/test.py
index 8a3b4a4..c8ff201 100755
--- a/development/build_log_simplifier/test.py
+++ b/development/build_log_simplifier/test.py
@@ -16,6 +16,7 @@
from build_log_simplifier import collapse_consecutive_blank_lines
from build_log_simplifier import collapse_tasks_having_no_output
+from build_log_simplifier import extract_task_names
from build_log_simplifier import remove_unmatched_exemptions
from build_log_simplifier import suggest_missing_exemptions
from build_log_simplifier import normalize_paths
@@ -70,6 +71,23 @@
assert(matcher.index_first_matching_regex("single") == 2)
assert(matcher.index_first_matching_regex("absent") is None)
+def test_detect_task_names():
+ print("test_detect_task_names")
+ lines = [
+ "> Task :one\n",
+ "some output\n",
+ "> Task :two\n",
+ "more output\n"
+ ]
+ task_names = [":one", ":two"]
+ detected_names = extract_task_names(lines)
+ if detected_names != task_names:
+ fail("extract_task_names returned incorrect response\n" +
+ "Input : " + str(lines) + "\n" +
+ "Output : " + str(detected_names) + "\n" +
+ "Expected: " + str(task_names)
+ )
+
def test_remove_unmatched_exemptions():
print("test_remove_unmatched_exemptions")
lines = [
@@ -261,6 +279,7 @@
def main():
test_collapse_consecutive_blank_lines()
test_collapse_tasks_having_no_output()
+ test_detect_task_names()
test_suggest_missing_exemptions()
test_normalize_paths()
test_regexes_matcher_get_matching_regexes()
diff --git a/development/simplify-build-failure/simplify-build-failure.sh b/development/simplify-build-failure/simplify-build-failure.sh
index ddd75ab..734a109 100755
--- a/development/simplify-build-failure/simplify-build-failure.sh
+++ b/development/simplify-build-failure/simplify-build-failure.sh
@@ -363,6 +363,9 @@
else
failed
fi
+ echo Copying minimal set of files into $fewestFilesOutputPath
+ rm -rf "$fewestFilesOutputPath"
+ cp -rT "$filtererStep1Output" "$fewestFilesOutputPath"
fi
if [ "$subfilePath" == "" ]; then
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 440faef..e2d581f 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -191,6 +191,7 @@
docs(project(":room:room-testing"))
docs(project(":savedstate:savedstate"))
docs(project(":savedstate:savedstate-ktx"))
+ docs(project(":security:security-app-authenticator"))
docs(project(":security:security-biometric"))
docs(project(":security:security-crypto"))
docs(project(":security:security-crypto-ktx"))
diff --git a/docs/api_guidelines.md b/docs/api_guidelines.md
new file mode 100644
index 0000000..3781200
--- /dev/null
+++ b/docs/api_guidelines.md
@@ -0,0 +1,1522 @@
+# Library API guidelines
+
+[TOC]
+
+## Introduction {#introduction}
+
+s.android.com/api-guidelines,
+which covers standard and practices for designing platform APIs.
+
+All platform API design guidelines also apply to Jetpack libraries, with any
+additional guidelines or exceptions noted in this document. Jetpack libraries
+also follow
+[explicit API mode](https://kotlinlang.org/docs/reference/whatsnew14.html#explicit-api-mode-for-library-authors)
+for Kotlin libraries.
+
+## Modules {#module}
+
+### Packaging and naming {#module-naming}
+
+Java packages within Jetpack follow the format `androidx.<feature-name>`. All
+classes within a feature's artifact must reside within this package, and may
+further subdivide into `androidx.<feature-name>.<layer>` using standard Android
+layers (app, widget, etc.) or layers specific to the feature.
+
+Maven artifacts use the groupId format `androidx.<feature-name>` and artifactId
+format `<feature-name>` to match the Java package.
+
+Sub-features that can be separated into their own artifact should use the
+following formats:
+
+Java package: `androidx.<feature-name>.<sub-feature>.<layer>`
+
+Maven groupId: `androidx.<feature-name>`
+
+Maven artifactId: `<feature-name>-<sub-feature>`
+
+#### Common sub-feature names {#module-naming-subfeature}
+
+* `-testing` for an artifact intended to be used while testing usages of your
+ library, e.g. `androidx.room:room-testing`
+* `-core` for a low-level artifact that *may* contain public APIs but is
+ primarily intended for use by other libraries in the group
+* `-ktx` for an Kotlin artifact that exposes idiomatic Kotlin APIs as an
+ extension to a Java-only library
+* `-java8` for a Java 8 artifact that exposes idiomatic Java 8 APIs as an
+ extension to a Java 7 library
+* `-<third-party>` for an artifact that integrates an optional third-party API
+ surface, e.g. `-proto` or `-rxjava2`. Note that a major version is included
+ in the sub-feature name for third-party API surfaces where the major version
+ indicates binary compatibility (only needed for post-1.x).
+
+Artifacts **should not** use `-impl` or `-base` to indicate that a library is an
+implementation detail shared within the group. Instead, use `-core`.
+
+#### Splitting existing modules
+
+Existing modules _should not_ be split into smaller modules; doing so creates
+the potential for class duplication issues when a developer depends on a new
+sub-module alongside the older top-level module. Consider the following
+scenario:
+
+* `androidx.library:1.0.0`
+ * contains classes `androidx.library.A` and `androidx.library.util.B`
+
+This module is split, moving `androidx.library.util.B` to a new module:
+
+* `androidx.library:1.1.0`
+ * contains class `androidx.library.A`
+ * depends on `androidx.library.util:1.0.0`
+* `androidx.library.util:1.0.0`
+ * depends on `androidx.library.util.B`
+
+A developer writes an app that depends directly on `androidx.library.util:1.0.0`
+and transitively pulls in `androidx.library:1.0.0`. Their app will no longer
+compile due to class duplication of `androidx.library.util.B`.
+
+While it is possible for the developer to fix this by manually specifying a
+dependency on `androidx.library:1.1.0`, there is no easy way for the developer
+to discover this solution from the class duplication error raised at compile
+time.
+
+#### Same-version (atomic) groups
+
+Library groups are encouraged to opt-in to a same-version policy whereby all
+libraries in the group use the same version and express exact-match dependencies
+on libraries within the group. Such groups must increment the version of every
+library at the same time and release all libraries at the same time.
+
+Atomic groups are specified in
+[`LibraryGroups.kt`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt):
+
+```kotlin
+// Non-atomic library group
+val APPCOMPAT = LibraryGroup("androidx.appcompat", null)
+// Atomic library group
+val APPSEARCH = LibraryGroup("androidx.appsearch", LibraryVersions.APPSEARCH)
+```
+
+Libraries within an atomic group should not specify a version in their
+`build.gradle`:
+
+```groovy
+androidx {
+ name = 'AppSearch'
+ publish = Publish.SNAPSHOT_AND_RELEASE
+ mavenGroup = LibraryGroups.APPSEARCH
+ inceptionYear = '2019'
+ description = 'Provides local and centralized app indexing'
+}
+```
+
+There is one exception to this policy. Newly-added libraries within an atomic
+group may stay within the `1.0.0-alphaXX` before conforming to the same-version
+policy. When the library would like to move to `beta`, it must match the version
+used by the atomic group (which must be `beta` at the time).
+
+The benefits of using an atomic group are:
+
+- Easier for developers to understand dependency versioning
+- `@RestrictTo(LIBRARY_GROUP)` APIs are treated as private APIs and not
+ tracked for binary compatibility
+- `@RequiresOptIn` APIs defined within the group may be used without any
+ restrictions between libraries in the group
+
+Potential drawbacks include:
+
+- All libraries within the group must be versioned identically at head
+- All libraries within the group must release at the same time
+
+
+### Choosing a `minSdkVersion` {#module-minsdkversion}
+
+The recommended minimum SDK version for new Jetpack libraries is currently
+**17** (Android 4.2, Jelly Bean). This SDK was chosen to represent 99% of active
+devices based on Play Store check-ins (see Android Studio
+[distribution metadata](https://dl.google.com/android/studio/metadata/distributions.json)
+for current statistics). This maximizes potential users for external developers
+while minimizing the amount of overhead necessary to support legacy versions.
+
+However, if no explicit minimum SDK version is specified for a library, the
+default is 14.
+
+Note that a library **must not** depend on another library with a higher
+`minSdkVersion` that its own, so it may be necessary for a new library to match
+its dependent libraries' `minSdkVersion`.
+
+Individual modules may choose a higher minimum SDK version for business or
+technical reasons. This is common for device-specific modules such as Auto or
+Wear.
+
+Individual classes or methods may be annotated with the
+[@RequiresApi](https://developer.android.com/reference/android/annotation/RequiresApi.html)
+annotation to indicate divergence from the overall module's minimum SDK version.
+Note that this pattern is _not recommended_ because it leads to confusion for
+external developers and should be considered a last-resort when backporting
+behavior is not feasible.
+
+## Platform compatibility API patterns {#platform-compatibility-apis}
+
+### Static shims (ex. [ViewCompat](https://developer.android.com/reference/android/support/v4/view/ViewCompat.html)) {#static-shim}
+
+When to use?
+
+* Platform class exists at module's `minSdkVersion`
+* Compatibility implementation does not need to store additional metadata
+
+Implementation requirements
+
+* Class name **must** be `<PlatformClass>Compat`
+* Package name **must** be `androidx.<feature>.<platform.package>`
+* Superclass **must** be `Object`
+* Class **must** be non-instantiable, i.e. constructor is private no-op
+* Static fields and static methods **must** match match signatures with
+ `PlatformClass`
+ * Static fields that can be inlined, ex. integer constants, **must not**
+ be shimmed
+* Public method names **must** match platform method names
+* Public methods **must** be static and take `PlatformClass` as first
+ parameter
+* Implementation _may_ delegate to `PlatformClass` methods when available
+
+#### Sample {#static-shim-sample}
+
+The following sample provides static helper methods for the platform class
+`android.os.Process`.
+
+```java
+/**
+ * Helper for accessing features in {@link Process}.
+ */
+public final class ProcessCompat {
+ private ProcessCompat() {
+ // This class is non-instantiable.
+ }
+
+ /**
+ * [Docs should match platform docs.]
+ *
+ * Compatibility behavior:
+ * <ul>
+ * <li>SDK 24 and above, this method matches platform behavior.
+ * <li>SDK 16 through 23, this method is a best-effort to match platform behavior, but may
+ * default to returning {@code true} if an accurate result is not available.
+ * <li>SDK 15 and below, this method always returns {@code true} as application UIDs and
+ * isolated processes did not exist yet.
+ * </ul>
+ *
+ * @param [match platform docs]
+ * @return [match platform docs], or a value based on platform-specific fallback behavior
+ */
+ public static boolean isApplicationUid(int uid) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ return Api24Impl.isApplicationUid(uid);
+ } else if (Build.VERSION.SDK_INT >= 17) {
+ return Api17Impl.isApplicationUid(uid);
+ } else if (Build.VERSION.SDK_INT == 16) {
+ return Api16Impl.isApplicationUid(uid);
+ } else {
+ return true;
+ }
+ }
+
+ @RequiresApi(24)
+ static class Api24Impl {
+ static boolean isApplicationUid(int uid) {
+ // In N, the method was made public on android.os.Process.
+ return Process.isApplicationUid(uid);
+ }
+ }
+
+ @RequiresApi(17)
+ static class Api17Impl {
+ private static Method sMethod_isAppMethod;
+ private static boolean sResolved;
+
+ static boolean isApplicationUid(int uid) {
+ // In JELLY_BEAN_MR2, the equivalent isApp(int) hidden method moved to public class
+ // android.os.UserHandle.
+ try {
+ if (!sResolved) {
+ sResolved = true;
+ sMethod_isAppMethod = UserHandle.class.getDeclaredMethod("isApp",int.class);
+ }
+ if (sMethod_isAppMethod != null) {
+ return (Boolean) sMethod_isAppMethod.invoke(null, uid);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return true;
+ }
+ }
+
+ ...
+}
+```
+
+### Wrapper (ex. [AccessibilityNodeInfoCompat](https://developer.android.com/reference/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.html)) {#wrapper}
+
+When to use?
+
+* Platform class may not exist at module's `minSdkVersion`
+* Compatibility implementation may need to store additional metadata
+* Needs to integrate with platform APIs as return value or method argument
+* **Note:** Should be avoided when possible, as using wrapper classes makes it
+ very difficult to deprecate classes and migrate source code when the
+ `minSdkVersion` is raised
+
+#### Sample {#wrapper-sample}
+
+The following sample wraps a hypothetical platform class `ModemInfo` that was
+added to the platform SDK in API level 23:
+
+```java
+public final class ModemInfoCompat {
+ // Only guaranteed to be non-null on SDK_INT >= 23. Note that referencing the
+ // class itself directly is fine -- only references to class members need to
+ // be pushed into static inner classes.
+ private final ModemInfo wrappedObj;
+
+ /**
+ * [Copy platform docs for matching constructor.]
+ */
+ public ModemInfoCompat() {
+ if (SDK_INT >= 23) {
+ wrappedObj = Api23Impl.create();
+ } else {
+ wrappedObj = null;
+ }
+ ...
+ }
+
+ @RequiresApi(23)
+ private ModemInfoCompat(@NonNull ModemInfo obj) {
+ mWrapped = obj;
+ }
+
+ /**
+ * Provides a backward-compatible wrapper for {@link ModemInfo}.
+ * <p>
+ * This method is not supported on devices running SDK < 23 since the platform
+ * class will not be available.
+ *
+ * @param info platform class to wrap
+ * @return wrapped class, or {@code null} if parameter is {@code null}
+ */
+ @RequiresApi(23)
+ @NonNull
+ public static ModemInfoCompat toModemInfoCompat(@NonNull ModemInfo info) {
+ return new ModemInfoCompat(obj);
+ }
+
+ /**
+ * Provides the {@link ModemInfo} represented by this object.
+ * <p>
+ * This method is not supported on devices running SDK < 23 since the platform
+ * class will not be available.
+ *
+ * @return platform class object
+ * @see ModemInfoCompat#toModemInfoCompat(ModemInfo)
+ */
+ @RequiresApi(23)
+ @NonNull
+ public ModemInfo toModemInfo() {
+ return mWrapped;
+ }
+
+ /**
+ * [Docs should match platform docs.]
+ *
+ * Compatibility behavior:
+ * <ul>
+ * <li>API level 23 and above, this method matches platform behavior.
+ * <li>API level 18 through 22, this method ...
+ * <li>API level 17 and earlier, this method always returns false.
+ * </ul>
+ *
+ * @return [match platform docs], or platform-specific fallback behavior
+ */
+ public boolean isLteSupported() {
+ if (SDK_INT >= 23) {
+ return Api23Impl.isLteSupported(mWrapped);
+ } else if (SDK_INT >= 18) {
+ // Smart fallback behavior based on earlier APIs.
+ ...
+ }
+ // Default behavior.
+ return false;
+ }
+
+ // All references to class members -- including the constructor -- must be
+ // made on an inner class to avoid soft-verification errors that slow class
+ // loading and prevent optimization.
+ @RequiresApi(23)
+ private static class Api23Impl {
+ @NonNull
+ static ModemInfo create() {
+ return new ModemInfo();
+ }
+
+ static boolean isLteSupported(PlatformClass obj) {
+ return obj.isLteSupported();
+ }
+ }
+}
+```
+
+Note that libraries written in Java should express conversion to and from the
+platform class differently than Kotlin classes. For Java classes, conversion
+from the platform class to the wrapper should be expressed as a `static` method,
+while conversion from the wrapper to the platform class should be a method on
+the wrapper object:
+
+```java
+@NonNull
+public static ModemInfoCompat toModemInfoCompat(@NonNull ModemInfo info);
+
+@NonNull
+public ModemInfo toModemInfo();
+```
+
+In cases where the primary library is written in Java and has an accompanying
+`-ktx` Kotlin extensions library, the following conversion should be provided as
+an extension function:
+
+```kotlin
+fun ModemInfo.toModemInfoCompat() : ModemInfoCompat
+```
+
+Whereas in cases where the primary library is written in Kotlin, the conversion
+should be provided as an extension factory:
+
+```kotlin
+class ModemInfoCompat {
+ fun toModemInfo() : ModemInfo
+
+ companion object {
+ @JvmStatic
+ @JvmName("toModemInfoCompat")
+ fun ModemInfo.toModemInfoCompat() : ModemInfoCompat
+ }
+}
+```
+
+#### API guidelines {#wrapper-api-guidelines}
+
+##### Naming {#wrapper-naming}
+
+* Class name **must** be `<PlatformClass>Compat`
+* Package name **must** be `androidx.core.<platform.package>`
+* Superclass **must not** be `<PlatformClass>`
+
+##### Construction {#wrapper-construction}
+
+* Class _may_ have public constructor(s) to provide parity with public
+ `PlatformClass` constructors
+ * Constructor used to wrap `PlatformClass` **must not** be public
+* Class **must** implement a static `PlatformClassCompat
+ toPlatformClassCompat(PlatformClass)` method to wrap `PlatformClass` on
+ supported SDK levels
+ * If class does not exist at module's `minSdkVersion`, method must be
+ annotated with `@RequiresApi(<sdk>)` for SDK version where class was
+ introduced
+
+#### Implementation {#wrapper-implementation}
+
+* Class **must** implement a `PlatformClass toPlatformClass()` method to
+ unwrap `PlatformClass` on supported SDK levels
+ * If class does not exist at module's `minSdkVersion`, method must be
+ annotated with `@RequiresApi(<sdk>)` for SDK version where class was
+ introduced
+* Implementation _may_ delegate to `PlatformClass` methods when available (see
+ below note for caveats)
+* To avoid runtime class verification issues, all operations that interact
+ with the internal structure of `PlatformClass` must be implemented in inner
+ classes targeted to the SDK level at which the operation was added.
+ * See the [sample](#wrapper-sample) for an example of interacting with a
+ method that was added in SDK level 23.
+
+### Standalone (ex. [ArraySet](https://developer.android.com/reference/android/support/v4/util/ArraySet.html), [Fragment](https://developer.android.com/reference/android/support/v4/app/Fragment.html)) {#standalone}
+
+When to use?
+
+* Platform class may exist at module's `minSdkVersion`
+* Does not need to integrate with platform APIs
+* Does not need to coexist with platform class, ex. no potential `import`
+ collision due to both compatibility and platform classes being referenced
+ within the same source file
+
+Implementation requirements
+
+* Class name **must** be `<PlatformClass>`
+* Package name **must** be `androidx.<platform.package>`
+* Superclass **must not** be `<PlatformClass>`
+* Class **must not** expose `PlatformClass` in public API
+* Implementation _may_ delegate to `PlatformClass` methods when available
+
+### Standalone JAR library (no Android dependencies) {#standalone-jar-library-no-android-dependencies}
+
+When to use
+
+* General purpose library with minimal interaction with Android types
+ * or when abstraction around types can be used (e.g. Room's SQLite
+ wrapper)
+* Lib used in parts of app with minimal Android dependencies
+ * ex. Repository, ViewModel
+* When Android dependency can sit on top of common library
+* Clear separation between android dependent and independent parts of your
+ library
+* Clear that future integration with android dependencies can be layered
+ separately
+
+**Examples:**
+
+The **Paging Library** pages data from DataSources (such as DB content from Room
+or network content from Retrofit) into PagedLists, so they can be presented in a
+RecyclerView. Since the included Adapter receives a PagedList, and there are no
+other Android dependencies, Paging is split into two parts - a no-android
+library (paging-common) with the majority of the paging code, and an android
+library (paging-runtime) with just the code to present a PagedList in a
+RecyclerView Adapter. This way, tests of Repositories and their components can
+be tested in host-side tests.
+
+**Room** loads SQLite data on Android, but provides an abstraction for those
+that want to use a different SQL implementation on device. This abstraction, and
+the fact that Room generates code dynamically, means that Room interfaces can be
+used in host-side tests (though actual DB code should be tested on device, since
+DB impls may be significantly different on host).
+
+## Implementing compatibility {#compat}
+
+### Referencing new APIs {#compat-newapi}
+
+Generally, methods on extension library classes should be available to all
+devices above the library's `minSdkVersion`.
+
+#### Checking device SDK version {#compat-sdk}
+
+The most common way of delegating to platform or backport implementations is to
+compare the device's `Build.VERSION.SDK_INT` field to a known-good SDK version;
+for example, the SDK in which a method first appeared or in which a critical bug
+was first fixed.
+
+Non-reflective calls to new APIs gated on `SDK_INT` **must** be made from
+version-specific static inner classes to avoid verification errors that
+negatively affect run-time performance. For more information, see Chromium's
+guide to
+[Class Verification Failures](https://chromium.googlesource.com/chromium/src/+/HEAD/build/android/docs/class_verification_failures.md).
+
+Methods in implementation-specific classes **must** be paired with the
+`@DoNotInline` annotation to prevent them from being inlined.
+
+```java {.good}
+public static void saveAttributeDataForStyleable(@NonNull View view, ...) {
+ if (Build.VERSION.SDK_INT >= 29) {
+ Api29Impl.saveAttributeDataForStyleable(view, ...);
+ }
+}
+
+@RequiresApi(29)
+private static class Api29Impl {
+ @DoNotInline
+ static void saveAttributeDataForStyleable(@NonNull View view, ...) {
+ view.saveAttributeDataForStyleable(...);
+ }
+}
+```
+
+Alternatively, in Kotlin sources:
+
+```kotlin {.good}
+@RequiresApi(29)
+object Api25 {
+ @DoNotInline
+ fun saveAttributeDataForStyleable(view: View, ...) { ... }
+}
+```
+
+When developing against pre-release SDKs where the `SDK_INT` has not been
+finalized, SDK checks **must** use `BuildCompat.isAtLeastX()` methods.
+
+```java {.good}
+@NonNull
+public static List<Window> getAllWindows() {
+ if (BuildCompat.isAtLeastR()) {
+ return ApiRImpl.getAllWindows();
+ }
+ return Collections.emptyList();
+}
+```
+
+#### Device-specific issues {#compat-oem}
+
+Library code may work around device- or manufacturer-specific issues -- issues
+not present in AOSP builds of Android -- *only* if a corresponding CTS test
+and/or CDD policy is added to the next revision of the Android platform. Doing
+so ensures that such issues can be detected and fixed by OEMs.
+
+#### Handling `minSdkVersion` disparity {#compat-minsdk}
+
+Methods that only need to be accessible on newer devices, including
+`to<PlatformClass>()` methods, may be annotated with `@RequiresApi(<sdk>)` to
+indicate they will fail to link on older SDKs. This annotation is enforced at
+build time by Lint.
+
+#### Handling `targetSdkVersion` behavior changes {#compat-targetsdk}
+
+To preserve application functionality, device behavior at a given API level may
+change based on an application's `targetSdkVersion`. For example, if an app with
+`targetSdkVersion` set to API level 22 runs on a device with API level 29, all
+required permissions will be granted at installation time and the run-time
+permissions framework will emulate earlier device behavior.
+
+Libraries do not have control over the app's `targetSdkVersion` and -- in rare
+cases -- may need to handle variations in platform behavior. Refer to the
+following pages for version-specific behavior changes:
+
+* API level 29:
+ [Android Q behavior changes: apps targeting Q](https://developer.android.com/preview/behavior-changes-q)
+* API level 28:
+ [Behavior changes: apps targeting API level 28+](https://developer.android.com/about/versions/pie/android-9.0-changes-28)
+* API level 26:
+ [Changes for apps targeting Android 8.0](https://developer.android.com/about/versions/oreo/android-8.0-changes#o-apps)
+* API level 24:
+ [Changes for apps targeting Android 7.0](https://developer.android.com/about/versions/nougat/android-7.0-changes#n-apps)
+* API level 21:
+ [Android 5.0 Behavior Changes](https://developer.android.com/about/versions/android-5.0-changes)
+* API level 19:
+ [Android 4.4 APIs](https://developer.android.com/about/versions/android-4.4)
+
+#### Working around Lint issues {#compat-lint}
+
+In rare cases, Lint may fail to interpret API usages and yield a `NewApi` error
+and require the use of `@TargetApi` or `@SuppressLint('NewApi')` annotations.
+Both of these annotations are strongly discouraged and may only be used
+temporarily. They **must never** be used in a stable release. Any usage of these
+annotation **must** be associated with an active bug, and the usage must be
+removed when the bug is resolved.
+
+### Delegating to API-specific implementations {#delegating-to-api-specific-implementations}
+
+#### SDK-dependent reflection
+
+Starting in API level 28, the platform restricts which
+[non-SDK interfaces](https://developer.android.com/distribute/best-practices/develop/restrictions-non-sdk-interfaces)
+can be accessed via reflection by apps and libraries. As a general rule, you
+will **not** be able to use reflection to access hidden APIs on devices with
+`SDK_INT` greater than `Build.VERSION_CODES.P` (28).
+
+On earlier devices, reflection on hidden platform APIs is allowed **only** when
+an alternative public platform API exists in a later revision of the Android
+SDK. For example, the following implementation is allowed:
+
+```java
+public AccessibilityDelegate getAccessibilityDelegate(View v) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ // Retrieve the delegate using a public API.
+ return v.getAccessibilityDelegate();
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ // Retrieve the delegate by reflecting on a private field. If the
+ // field does not exist or cannot be accessed, this will no-op.
+ if (sAccessibilityDelegateField == null) {
+ try {
+ sAccessibilityDelegateField = View.class
+ .getDeclaredField("mAccessibilityDelegate");
+ sAccessibilityDelegateField.setAccessible(true);
+ } catch (Throwable t) {
+ sAccessibilityDelegateCheckFailed = true;
+ return null;
+ }
+ }
+ try {
+ Object o = sAccessibilityDelegateField.get(v);
+ if (o instanceof View.AccessibilityDelegate) {
+ return (View.AccessibilityDelegate) o;
+ }
+ return null;
+ } catch (Throwable t) {
+ sAccessibilityDelegateCheckFailed = true;
+ return null;
+ }
+ } else {
+ // There is no way to retrieve the delegate, even via reflection.
+ return null;
+ }
+```
+
+Calls to public APIs added in pre-release revisions *must* be gated using
+`BuildCompat`:
+
+```java
+if (BuildCompat.isAtLeastQ()) {
+ // call new API added in Q
+} else if (Build.SDK_INT.VERSION >= Build.VERSION_CODES.SOME_RELEASE) {
+ // make a best-effort using APIs that we expect to be available
+} else {
+ // no-op or best-effort given no information
+}
+```
+
+### Inter-process communication {#inter-process-communication}
+
+Protocols and data structures used for IPC must support interoperability between
+different versions of libraries and should be treated similarly to public API.
+
+#### Data structures
+
+**Do not** use Parcelable for any class that may be used for IPC or otherwise
+exposed as public API. The data format used by Parcelable does not provide any
+compatibility guarantees and will result in crashes if fields are added or
+removed between library versions.
+
+**Do not** design your own serialization mechanism or wire format for disk
+storage or inter-process communication. Preserving and verifying compatibility
+is difficult and error-prone.
+
+If you expose a `Bundle` to callers that can cross processes, you should
+[prevent apps from adding their own custom parcelables](https://android.googlesource.com/platform/frameworks/base/+/6cddbe14e1ff67dc4691a013fe38a2eb0893fe03)
+as top-level entries; if *any* entry in a `Bundle` can't be loaded, even if it's
+not actually accessed, the receiving process is likely to crash.
+
+**Do** use protocol buffers or, in some simpler cases, `VersionedParcelable`.
+
+#### Communication protocols
+
+Any communication prototcol, handshake, etc. must maintain compatibility
+consistent with SemVer guidelines. Consider how your protocol will handle
+addition and removal of operations or constants, compatibility-breaking changes,
+and other modifications without crashing either the host or client process.
+
+## Deprecation and removal
+
+While SemVer's binary compatibility guarantees restrict the types of changes
+that may be made within a library revision and make it difficult to remove an
+API, there are many other ways to influence how developers interact with your
+library.
+
+### Deprecation (`@deprecated`)
+
+Deprecation lets a developer know that they should stop using an API or class.
+All deprecations must be marked with a `@Deprecated` Java annotation as well as
+a `@deprecated <migration-docs>` docs annotation explaining how the developer
+should migrate away from the API.
+
+Deprecation is an non-breaking API change that must occur in a **major** or
+**minor** release.
+
+### Soft removal (@removed)
+
+Soft removal preserves binary compatibility while preventing source code from
+compiling against an API. It is a *source-breaking change* and not recommended.
+
+Soft removals **must** do the following:
+
+* Mark the API as deprecated for at least one stable release prior to removal.
+* Mark the API with a `@RestrictTo(LIBRARY)` Java annotation as well as a
+ `@removed <reason>` docs annotation explaining why the API was removed.
+* Maintain binary compatibility, as the API may still be called by existing
+ dependent libraries.
+* Maintain behavioral compatibility and existing tests.
+
+This is a disruptive change and should be avoided when possible.
+
+Soft removal is a source-breaking API change that must occur in a **major** or
+**minor** release.
+
+### Hard removal
+
+Hard removal entails removing the entire implementation of an API that was
+exposed in a public release. Prior to removal, an API must be marked as
+`@deprecated` for a full **minor** version (`alpha`->`beta`->`rc`->stable),
+prior to being hard removed.
+
+This is a disruptive change and should be avoided when possible.
+
+Hard removal is a binary-breaking API change that must occur in a **major**
+release.
+
+### For entire artifacts
+
+We do not typically deprecate or remove entire artifacts; however, it may be
+useful in cases where we want to halt development and focus elsewhere or
+strongly discourage developers from using a library.
+
+Halting development, either because of staffing or prioritization issues, leaves
+the door open for future bug fixes or continued development. This quite simply
+means we stop releasing updates but retain the source in our tree.
+
+Deprecating an artifact provides developers with a migration path and strongly
+encourages them -- through Lint warnings -- to migrate elsewhere. This is
+accomplished by adding a `@Deprecated` and `@deprecated` (with migration
+comment) annotation pair to *every* class and interface in the artifact.
+
+The fully-deprecated artifact will be released as a deprecation release -- it
+will ship normally with accompanying release notes indicating the reason for
+deprecation and migration strategy, and it will be the last version of the
+artifact that ships. It will ship as a new minor stable release. For example, if
+`1.0.0` was the last stable release, then the deprecation release will be
+`1.1.0`. This is so Android Studio users will get a suggestion to update to a
+new stable version, which will contain the `@deprecated` annotations.
+
+After an artifact has been released as fully-deprecated, it can be removed from
+the source tree.
+
+## Resources {#resources}
+
+Generally, follow the official Android guidelines for
+[app resources](https://developer.android.com/guide/topics/resources/providing-resources).
+Special guidelines for library resources are noted below.
+
+### Defining new resources
+
+Libraries may define new value and attribute resources using the standard
+application directory structure used by Android Gradle Plugin:
+
+```
+src/main/res/
+ values/
+ attrs.xml Theme attributes and styleables
+ dimens.xml Dimensional values
+ public.xml Public resource definitions
+ ...
+```
+
+However, some libraries may still be using non-standard, legacy directory
+structures such as `res-public` for their public resource declarations or a
+top-level `res` directory and accompanying custom source set in `build.gradle`.
+These libraries will eventually be migrated to follow standard guidelines.
+
+#### Naming conventions
+
+Libraries follow the Android platform's resource naming conventions, which use
+`camelCase` for attributes and `underline_delimited` for values. For example,
+`R.attr.fontProviderPackage` and `R.dimen.material_blue_grey_900`.
+
+#### Attribute formats
+
+At build time, attribute definitions are pooled globally across all libraries
+used in an application, which means attribute `format`s *must* be identical for
+a given `name` to avoid a conflict.
+
+Within Jetpack, new attribute names *must* be globally unique. Libraries *may*
+reference existing public attributes from their dependencies. See below for more
+information on public attributes.
+
+When adding a new attribute, the format should be defined *once* in an `<attr
+/>` element in the definitions block at the top of `src/main/res/attrs.xml`.
+Subsequent references in `<declare-styleable>` elements *must* not include a
+`format`:
+
+`src/main/res/attrs.xml`
+
+```xml
+<resources>
+ <attr name="fontProviderPackage" format="string" />
+
+ <declare-styleable name="FontFamily">
+ <attr name="fontProviderPackage" />
+ </declare-styleable>
+</resources>
+```
+
+### Public resources
+
+Library resources are private by default, which means developers are discouraged
+from referencing any defined attributes or values from XML or code; however,
+library resources may be declared public to make them available to developers.
+
+Public library resources are considered API surface and are thus subject to the
+same API consistency and documentation requirements as Java APIs.
+
+Libraries will typically only expose theme attributes, ex. `<attr />` elements,
+as public API so that developers can set and retrieve the values stored in
+styles and themes. Exposing values -- such as `<dimen />` and `<string />` -- or
+images -- such as drawable XML and PNGs -- locks the current state of those
+elements as public API that cannot be changed without a major version bump. That
+means changing a publicly-visible icon would be considered a breaking change.
+
+#### Documentation
+
+All public resource definitions should be documented, including top-level
+definitions and re-uses inside `<styleable>` elements:
+
+`src/main/res/attrs.xml`
+
+```xml
+<resources>
+ <!-- String specifying the application package for a Font Provider. -->
+ <attr name="fontProviderPackage" format="string" />
+
+ <!-- Attributes that are read when parsing a <fontfamily> tag. -->
+ <declare-styleable name="FontFamily">
+ <!-- The package for the Font Provider to be used for the request. This is
+ used to verify the identity of the provider. -->
+ <attr name="fontProviderPackage" />
+ </declare-styleable>
+</resources>
+```
+
+`src/main/res/colors.xml`
+
+```xml
+<resources>
+ <!-- Color for Material Blue-Grey 900. -->
+ <color name="material_blue_grey_900">#ff263238</color>
+</resources>
+```
+
+#### Public declaration
+
+Resources are declared public by providing a separate `<public />` element with
+a matching type:
+
+`src/main/res/public.xml`
+
+```xml
+<resources>
+ <public name="fontProviderPackage" type="attr" />
+ <public name="material_blue_grey_900" type="color" />
+</resources>
+```
+
+#### More information
+
+See also the official Android Gradle Plugin documentation for
+[Private Resources](https://developer.android.com/studio/projects/android-library#PrivateResources).
+
+### Manifest entries (`AndroidManifest.xml`) {#resources-manifest}
+
+#### Metadata tags (`<meta-data>`) {#resources-manifest-metadata}
+
+Developers **must not** add `<application>`-level `<meta-data>` tags to library
+manifests or advise developers to add such tags to their application manifests.
+Doing so may _inadvertently cause denial-of-service attacks against other apps_.
+
+Assume a library adds a single item of meta-data at the application level. When
+an app uses the library, that meta-data will be merged into the resulting app's
+application entry via manifest merger.
+
+If another app attempts to obtain a list of all activities associated with the
+primary app, that list will contain multiple copies of the `ApplicationInfo`,
+each of which in turn contains a copy of the library's meta-data. As a result,
+one `<metadata>` tag may become hundreds of KB on the binder call to obtain the
+list -- resulting in apps hitting transaction too large exceptions and crashing.
+
+```xml {.bad}
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.librarypackage">
+ <application>
+ <meta-data
+ android:name="keyName"
+ android:value="@string/value" />
+ </application>
+</manifest>
+```
+
+Instead, developers may consider adding `<metadata>` nested inside of
+placeholder `<service>` tags.
+
+```xml {.good}
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.librarypackage">
+ <application>
+ <service
+ android:name="androidx.librarypackage.MetadataHolderService"
+ android:enabled="false"
+ android:exported="false">
+ <meta-data
+ android:name="androidx.librarypackage.MetadataHolderService.KEY_NAME"
+ android:resource="@string/value" />
+ </service>
+ </application>
+```
+
+```java {.good}
+package androidx.libraryname.featurename;
+
+/**
+ * A placeholder service to avoid adding application-level metadata. The service
+ * is only used to expose metadata defined in the library's manifest. It is
+ * never invoked.
+ */
+public final class MetadataHolderService {
+ private MetadataHolderService() {}
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ throw new UnsupportedOperationException();
+ }
+}
+```
+
+## Dependencies {#dependencies}
+
+Generally, Jetpack libraries should avoid dependencies that negatively impact
+developers without providing substantial benefit. This includes large
+dependencies where only a small portion is needed, dependencies that slow down
+build times through annotation processing or compiler overhead, and generally
+any dependency that negatively affects system health.
+
+### Kotlin {#dependencies-kotlin}
+
+Kotlin is _recommended_ for new libraries; however, it's important to consider
+its size impact on clients. Currently, the Kotlin stdlib adds a minimum of 40kB
+post-optimization.
+
+### Kotlin coroutines {#dependencies-coroutines}
+
+Kotlin's coroutine library adds around 100kB post-shrinking. New libraries that
+are written in Kotlin should prefer coroutines over `ListenableFuture`, but
+existing libraries must consider the size impact on their clients. See
+[Asynchronous work with return values](#async-return) for more details on using
+Kotlin coroutines in Jetpack libraries.
+
+### Guava {#dependencies-guava}
+
+The full Guava library is very large and *must not* be used. Libraries that
+would like to depend on Guava's `ListenableFuture` may instead depend on the
+standalone `com.google.guava:listenablefuture` artifact. See
+[Asynchronous work with return values](#async-return) for more details on using
+`ListenableFuture` in Jetpack libraries.
+
+### Java 8 {#dependencies-java8}
+
+Libraries that take a dependency on a library targeting Java 8 must _also_
+target Java 8, which will incur a ~5% build performance (as of 8/2019) hit for
+clients. New libraries targeting Java 8 may use Java 8 dependencies; however,
+existing libraries targeting Java 7 should not.
+
+The default language level for `androidx` libraries is Java 8, and we encourage
+libraries to stay on Java 8. However, if you have a business need to target Java
+7, you can specify Java 7 in your `build.gradle` as follows:
+
+```Groovy
+android {
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_7
+ targetCompatibility = JavaVersion.VERSION_1_7
+ }
+}
+```
+
+## More API guidelines {#more-api-guidelines}
+
+### Annotations {#annotation}
+
+#### Annotation processors {#annotation-processor}
+
+Annotation processors should opt-in to incremental annotation processing to
+avoid triggering a full recompilation on every client source code change. See
+Gradle's
+[Incremental annotation processing](https://docs.gradle.org/current/userguide/java_plugin.html#sec:incremental_annotation_processing)
+documentation for information on how to opt-in.
+
+### Experimental APIs {#experimental-api}
+
+Jetpack libraries may choose to annotate API surfaces as unstable using either
+Kotlin's
+[`@Experimental` annotation](https://kotlinlang.org/docs/reference/experimental.html)
+for APIs written in Kotlin or Jetpack's
+[`@Experimental` annotation](https://developer.android.com/reference/kotlin/androidx/annotation/experimental/Experimental)
+for APIs written in Java.
+
+In both cases, API surfaces marked as experimental are considered alpha and will
+be excluded from API compatibility guarantees. Due to the lack of compatibility
+guarantees, libraries *must never* call experimental APIs exposed by other
+libraries and *may not* use the `@UseExperimental` annotation except in the
+following cases:
+
+* A library within a same-version group *may* call an experimental API exposed
+ by another library **within its same-version group**. In this case, API
+ compatibility guarantees are covered under the same-version group policies
+ and the library *may* use the `@UsesExperimental` annotation to prevent
+ propagation of the experimental property. **Library owners must exercise
+ care to ensure that post-alpha APIs backed by experimental APIs actually
+ meet the release criteria for post-alpha APIs.**
+
+#### How to mark an API surface as experimental
+
+All libraries using `@Experimental` annotations *must* depend on the
+`androidx.annotation:annotation-experimental` artifact regardless of whether
+they are using the `androidx` or Kotlin annotation. This artifact provides Lint
+enforcement of experimental usage restrictions for Kotlin callers as well as
+Java (which the Kotlin annotation doesn't handle on its own, since it's a Kotlin
+compiler feature). Libraries *may* include the dependency as `api`-type to make
+`@UseExperimental` available to Java clients; however, this will also
+unnecessarily expose the `@Experimental` annotation.
+
+```java
+dependencies {
+ implementation(project(":annotation:annotation-experimental"))
+}
+```
+
+See Kotlin's
+[experimental marker documentation](https://kotlinlang.org/docs/reference/experimental.html)
+for general usage information. If you are writing experimental Java APIs, you
+will use the Jetpack
+[`@Experimental` annotation](https://developer.android.com/reference/kotlin/androidx/annotation/experimental/Experimental)
+rather than the Kotlin compiler's annotation.
+
+#### How to transition an API out of experimental
+
+When an API surface is ready to transition out of experimental, the annotation
+may only be removed during an alpha pre-release stage since removing the
+experimental marker from an API is equivalent to adding the API to the current
+API surface.
+
+When transitioning an entire feature surface out of experimental, you *should*
+remove the associated annotations.
+
+When making any change to the experimental API surface, you *must* run
+`./gradlew updateApi` prior to uploading your change.
+
+### Restricted APIs {#restricted-api}
+
+Jetpack's library tooling supports hiding Java-visible (ex. `public` and
+`protected`) APIs from developers using a combination of the `@hide` docs
+annotation and `@RestrictTo` source annotation. These annotations **must** be
+paired together when used, and are validated as part of presubmit checks for
+Java code (Kotlin not yet supported by Checkstyle).
+
+The effects of hiding an API are as follows:
+
+* The API will not appear in documentation
+* Android Studio will warn the developer not to use the API
+
+Hiding an API does *not* provide strong guarantees about usage:
+
+* There are no runtime restrictions on calling hidden APIs
+* Android Studio will not warn if hidden APIs are called using reflection
+* Hidden APIs will still show in Android Studio's auto-complete
+
+#### When to use `@hide` {#restricted-api-usage}
+
+Generally, avoid using `@hide`. The `@hide` annotation indicates that developers
+should not call an API that is _technically_ public from a Java visibility
+perspective. Hiding APIs is often a sign of a poorly-abstracted API surface, and
+priority should be given to creating public, maintainable APIs and using Java
+visibility modifiers.
+
+*Do not* use `@hide` to bypass API tracking and review for production APIs;
+instead, rely on API+1 and API Council review to ensure APIs are reviewed on a
+timely basis.
+
+*Do not* use `@hide` for implementation detail APIs that are used between
+libraries and could reasonably be made public.
+
+*Do* use `@hide` paired with `@RestrictTo(LIBRARY)` for implementation detail
+APIs used within a single library (but prefer Java language `private` or
+`default` visibility).
+
+#### `RestrictTo.Scope` and inter- versus intra-library API surfaces {#private-api-types}
+
+To maintain binary compatibility between different versions of libraries,
+restricted API surfaces that are used between libraries (inter-library APIs)
+must follow the same Semantic Versioning rules as public APIs. Inter-library
+APIs should be annotated with the `@RestrictTo(LIBRARY_GROUP)` source
+annotation.
+
+Restricted API surfaces used within a single library (intra-library APIs), on
+the other hand, may be added or removed without any compatibility
+considerations. It is safe to assume that developers _never_ call these APIs,
+even though it is technically feasible. Intra-library APIs should be annotated
+with the `@RestrictTo(LIBRARY)` source annotation.
+
+The following table shows the visibility of a hypothetical API within Maven
+coordinate `androidx.concurrent:concurrent` when annotated with a variety of
+scopes:
+
+<table>
+ <tr>
+ <td><code>RestrictTo.Scope</code></td>
+ <td>Visibility by Maven coordinate</td>
+ </tr>
+ <tr>
+ <td><code>LIBRARY</code></td>
+ <td><code>androidx.concurrent:concurrent</code></td>
+ </tr>
+ <tr>
+ <td><code>LIBRARY_GROUP</code></td>
+ <td><code>androidx.concurrent:*</code></td>
+ </tr>
+ <tr>
+ <td><code>LIBRARY_GROUP_PREFIX</code></td>
+ <td><code>androidx.*:*</code></td>
+ </tr>
+</table>
+
+### Constructors {#constructors}
+
+#### View constructors {#view-constructors}
+
+The four-arg View constructor -- `View(Context, AttributeSet, int, int)` -- was
+added in SDK 21 and allows a developer to pass in an explicit default style
+resource rather than relying on a theme attribute to resolve the default style
+resource. Because this API was added in SDK 21, care must be taken to ensure
+that it is not called through any < SDK 21 code path.
+
+Views _may_ implement a four-arg constructor in one of the following ways:
+
+1. Do not implement.
+1. Implement and annotate with `@RequiresApi(21)`. This means the three-arg
+ constructor **must not** call into the four-arg constructor.
+
+### Asynchronous work {#async}
+
+#### With return values {#async-return}
+
+Traditionally, asynchronous work on Android that results in an output value
+would use a callback; however, better alternatives exist for libraries.
+
+Kotlin libraries should prefer
+[coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) and
+`suspend` functions, but please refer to the guidance on
+[allowable dependencies](#dependencies-coroutines) before adding a new
+dependency on coroutines.
+
+Java libraries should prefer `ListenableFuture` and the
+[`CallbackToFutureAdapter`](https://developer.android.com/reference/androidx/concurrent/futures/CallbackToFutureAdapter)
+implementation provided by the `androidx.concurrent:concurrent-futures` library.
+
+Libraries **must not** use `java.util.concurrent.CompletableFuture`, as it has a
+large API surface that permits arbitrary mutation of the future's value and has
+error-prone defaults.
+
+See the [Dependencies](#dependencies) section for more information on using
+Kotlin coroutines and Guava in your library.
+
+#### Avoid `synchronized` methods
+
+Whenever multiple threads are interacting with shared (mutable) references those
+reads and writes must be synchronized in some way. However synchronized blocks
+make your code thread-safe at the expense of concurrent execution. Any time
+execution enters a synchronized block or method any other thread trying to enter
+a synchronized block on the same object has to wait; even if in practice the
+operations are unrelated (e.g. they interact with different fields). This can
+dramatically reduce the benefit of trying to write multi-threaded code in the
+first place.
+
+Locking with synchronized is a heavyweight form of ensuring ordering between
+threads, and there are a number of common APIs and patterns that you can use
+that are more lightweight, depending on your use case:
+
+* Compute a value once and make it available to all threads
+* Update Set and Map data structures across threads
+* Allow a group of threads to process a stream of data concurrently
+* Provide instances of a non-thread-safe type to multiple threads
+* Update a value from multiple threads atomically
+* Maintain granular control of your concurrency invariants
+
+### Kotlin {#kotlin}
+
+#### Data classes {#kotlin-data}
+
+Kotlin `data` classes provide a convenient way to define simple container
+objects, where Kotlin will generate `equals()` and `hashCode()` for you.
+However, they are not designed to preserve API/binary compatibility when members
+are added. This is due to other methods which are generated for you -
+[destructuring declarations](https://kotlinlang.org/docs/reference/multi-declarations.html),
+and [copying](https://kotlinlang.org/docs/reference/data-classes.html#copying).
+
+Example data class as tracked by metalava:
+
+<pre>
+ public final class TargetAnimation {
+ ctor public TargetAnimation(float target, androidx.animation.AnimationBuilder animation);
+ <b>method public float component1();</b>
+ <b>method public androidx.animation.AnimationBuilder component2();</b>
+ <b>method public androidx.animation.TargetAnimation copy(float target, androidx.animation.AnimationBuilder animation);</b>
+ method public androidx.animation.AnimationBuilder getAnimation();
+ method public float getTarget();
+ }
+</pre>
+
+Because members are exposed as numbered components for destructuring, you can
+only safely add members at the end of the member list. As `copy` is generated
+with every member name in order as well, you'll also have to manually
+re-implement any old `copy` variants as items are added. If these constraints
+are acceptable, data classes may still be useful to you.
+
+As a result, Kotlin `data` classes are _strongly discouraged_ in library APIs.
+Instead, follow best-practices for Java data classes including implementing
+`equals`, `hashCode`, and `toString`.
+
+See Jake Wharton's article on
+[Public API challenges in Kotlin](https://jakewharton.com/public-api-challenges-in-kotlin/)
+for more details.
+
+#### Extension and top-level functions {#kotlin-extension-functions}
+
+If your Kotlin file contains any sybmols outside of class-like types
+(extension/top-level functions, properties, etc), the file must be annotated
+with `@JvmName`. This ensures unanticipated use-cases from Java callers don't
+get stuck using `BlahKt` files.
+
+Example:
+
+```kotlin {.bad}
+package androidx.example
+
+fun String.foo() = // ...
+```
+
+```kotlin {.good}
+@file:JvmName("StringUtils")
+
+package androidx.example
+
+fun String.foo() = // ...
+```
+
+NOTE This guideline may be ignored for libraries that only work in Kotlin (think
+Compose).
+
+## Testing Guidelines
+
+### [Do not Mock, AndroidX](do_not_mock.md)
+
+## Android Lint Guidelines
+
+### Suppression vs Baselines
+
+Lint sometimes flags false positives, even though it is safe to ignore these
+errors (for example WeakerAccess warnings when you are avoiding synthetic
+access). There may also be lint failures when your library is in the middle of a
+beta / rc / stable release, and cannot make the breaking changes needed to fix
+the root cause. There are two ways of ignoring lint errors:
+
+1. Suppression - using `@SuppressLint` (for Java) or `@Suppress` annotations to
+ ignore the warning per call site, per method, or per file. *Note
+ `@SuppressLint` - Requires Android dependency*.
+2. Baselines - allowlisting errors in a lint-baseline.xml file at the root of
+ the project directory.
+
+Where possible, you should use a **suppression annotation at the call site**.
+This helps ensure that you are only suppressing the *exact* failure, and this
+also keeps the failure visible so it can be fixed later on. Only use a baseline
+if you are in a Java library without Android dependencies, or when enabling a
+new lint check, and it is prohibitively expensive / not possible to fix the
+errors generated by enabling this lint check.
+
+To update a lint baseline (lint-baseline.xml) after you have fixed issues, add
+`-PupdateLintBaseline` to the end of your lint command. This will delete and
+then regenerate the baseline file.
+
+```shell
+./gradlew core:lintDebug -PupdateLintBaseline
+```
+
+## Metalava API Lint
+
+As well as Android Lint, which runs on all source code, Metalava will also run
+checks on the public API surface of each library. Similar to with Android Lint,
+there can sometimes be false positives / intended deviations from the API
+guidelines that Metalava will lint your API surface against. When this happens,
+you can suppress Metalava API lint issues using `@SuppressLint` (for Java) or
+`@Suppress` annotations. In cases where it is not possible, update Metalava's
+baseline with the `updateApiLintBaseline` task.
+
+```shell
+./gradlew core:updateApiLintBaseline
+```
+
+This will create/amend the `api_lint.ignore` file that lives in a library's
+`api` directory.
+
+## Build Output Guidelines
+
+In order to more easily identify the root cause of build failures, we want to
+keep the amount of output generated by a successful build to a minimum.
+Consequently, we track build output similarly to the way in which we track Lint
+warnings.
+
+### Invoking build output validation
+
+You can add `-Pandroidx.validateNoUnrecognizedMessages` to any other AndroidX
+gradlew command to enable validation of build output. For example:
+
+```shell
+/gradlew -Pandroidx.validateNoUnrecognizedMessages :help
+```
+
+### Exempting new build output messages
+
+Please avoid exempting new build output and instead fix or suppress the warnings
+themselves, because that will take effect not only on the build server but also
+in Android Studio, and will also run more quickly.
+
+If you cannot prevent the message from being generating and must exempt the
+message anyway, follow the instructions in the error:
+
+```shell
+$ ./gradlew -Pandroidx.validateNoUnrecognizedMessages :help
+
+Error: build_log_simplifier.py found 15 new messages found in /usr/local/google/workspace/aosp-androidx-git/out/dist/gradle.log.
+
+Please fix or suppress these new messages in the tool that generates them.
+If you cannot, then you can exempt them by doing:
+
+ 1. cp /usr/local/google/workspace/aosp-androidx-git/out/dist/gradle.log.ignore /usr/local/google/workspace/aosp-androidx-git/frameworks/support/development/build_log_simplifier/messages.ignore
+ 2. modify the new lines to be appropriately generalized
+```
+
+Each line in this exemptions file is a regular expressing matching one or more
+lines of output to be exempted. You may want to make these expressions as
+specific as possible to ensure that the addition of new, similar messages will
+also be detected (for example, discovering an existing warning in a new source
+file).
+
+## Behavior changes
+
+### Changes that affect API documentation
+
+Do not make behavior changes that require altering API documentation in a way
+that would break existing clients, even if such changes are technically binary
+compatible. For example, changing the meaning of a method's return value to
+return true rather than false in a given state would be considered a breaking
+change. Because this change is binary-compatible, it will not be caught by
+tooling and is effectively invisible to clients.
+
+Instead, add new methods and deprecate the existing ones if necessary, noting
+behavior changes in the deprecation message.
+
+### High-risk behavior changes
+
+Behavior changes that conform to documented API contracts but are highly complex
+and difficult to comprehensively test are considered high-risk and should be
+implemented using behavior flags. These changes may be flagged on initially, but
+the original behaviors must be preserved until the library enters release
+candidate stage and the behavior changes have been appropriately verified by
+integration testing against public pre-release
+revisions.
+
+It may be necessary to soft-revert a high-risk behavior change with only 24-hour
+notice, which should be achievable by flipping the behavior flag to off.
+
+```java
+[example code pending]
+```
+
+Avoid adding multiple high-risk changes during a feature cycle, as verifying the
+interaction of multiple feature flags leads to unnecessary complexity and
+exposes clients to high risk even when a single change is flagged off. Instead,
+wait until one high-risk change has landed in RC before moving on to the next.
+
+#### Testing
+
+Relevant tests should be run for the behavior change in both the on and off
+flagged states to prevent regressions.
+
+## Sample code in Kotlin modules
+
+### Background
+
+Public API can (and should!) have small corresponding code snippets that
+demonstrate functionality and usage of a particular API. These are often exposed
+inline in the documentation for the function / class - this causes consistency
+and correctness issues as this code is not compiled against, and the underlying
+implementation can easily change.
+
+KDoc (JavaDoc for Kotlin) supports a `@sample` tag, which allows referencing the
+body of a function from documentation. This means that code samples can be just
+written as a normal function, compiled and linted against, and reused from other
+modules such as tests! This allows for some guarantees on the correctness of a
+sample, and ensuring that it is always kept up to date.
+
+### Enforcement
+
+There are still some visibility issues here - it can be hard to tell if a
+function is a sample, and is used from public documentation - so as a result we
+have lint checks to ensure sample correctness.
+
+Primarily, there are three requirements when using sample links:
+
+1. All functions linked to from a `@sample` KDoc tag must be annotated with
+ `@Sampled`
+2. All sample functions annotated with `@Sampled` must be linked to from a
+ `@sample` KDoc tag
+3. All sample functions must live inside a separate `samples` library
+ submodule - see the section on module configuration below for more
+ information.
+
+This enforces visibility guarantees, and make it easier to know that a sample is
+a sample. This also prevents orphaned samples that aren't used, and remain
+unmaintained and outdated.
+
+### Sample usage
+
+The follow demonstrates how to reference sample functions from public API. It is
+also recommended to reuse these samples in unit tests / integration tests / test
+apps / library demos where possible.
+
+**Public API:**
+
+```
+/*
+ * Fancy prints the given [string]
+ *
+ * @sample androidx.printer.samples.fancySample
+ */
+fun fancyPrint(str: String) ...
+```
+
+**Sample function:**
+
+```
+package androidx.printer.samples
+
+import androidx.printer.fancyPrint
+
+@Sampled
+fun fancySample() {
+ fancyPrint("Fancy!")
+}
+```
+
+**Generated documentation visible on d.android.com\***
+
+```
+fun fancyPrint(str: String)
+
+Fancy prints the given [string]
+
+<code>
+ import androidx.printer.fancyPrint
+
+ fancyPrint("Fancy!")
+<code>
+```
+
+\**still some improvements to be made to DAC side, such as syntax highlighting*
+
+### Module configuration
+
+The following module setups should be used for sample functions, and are
+enforced by lint:
+
+**Group-level samples**
+
+For library groups with strongly related samples that want to share code.
+
+Gradle project name: `:foo-library:samples`
+
+```
+foo-library/
+ foo-module/
+ bar-module/
+ samples/
+```
+
+**Per-module samples**
+
+For library groups with complex, relatively independent sub-libraries
+
+Gradle project name: `:foo-library:foo-module:samples`
+
+```
+foo-library/
+ foo-module/
+ samples/
+```
diff --git a/docs/benchmarking.md b/docs/benchmarking.md
new file mode 100644
index 0000000..cc38f78
--- /dev/null
+++ b/docs/benchmarking.md
@@ -0,0 +1,273 @@
+# Benchmarking in AndroidX
+
+[TOC]
+
+The public documentation at
+[d.android.com/benchmark](http://d.android.com/benchmark) explains how to use
+the library - this page focuses on specifics to writing libraries in the
+AndroidX repo, and our continuous testing / triage process.
+
+### Writing the benchmark
+
+Benchmarks are just regular instrumentation tests! Just use the
+[`BenchmarkRule`](https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/benchmark/junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt)
+provided by the library:
+
+<section class="tabs">
+
+#### Kotlin {.new-tab}
+
+```kotlin
+@RunWith(AndroidJUnit4::class)
+class ViewBenchmark {
+ @get:Rule
+ val benchmarkRule = BenchmarkRule()
+
+ @Test
+ fun simpleViewInflate() {
+ val context = InstrumentationRegistry
+ .getInstrumentation().targetContext
+ val inflater = LayoutInflater.from(context)
+ val root = FrameLayout(context)
+
+ benchmarkRule.measure {
+ inflater.inflate(R.layout.test_simple_view, root, false)
+ }
+ }
+}
+```
+
+#### Java {.new-tab}
+
+```java
+@RunWith(AndroidJUnit4.class)
+public class ViewBenchmark {
+ @Rule
+ public BenchmarkRule mBenchmarkRule = new BenchmarkRule();
+
+ @Test
+ public void simpleViewInflate() {
+ Context context = InstrumentationRegistry
+ .getInstrumentation().getTargetContext();
+ final BenchmarkState state = mBenchmarkRule.getState();
+ LayoutInflater inflater = LayoutInflater.from(context);
+ FrameLayout root = new FrameLayout(context);
+
+ while (state.keepRunning()) {
+ inflater.inflate(R.layout.test_simple_view, root, false);
+ }
+ }
+}
+```
+
+</section>
+
+## Project structure
+
+As in the public documentation, benchmarks in the AndroidX repo are test-only
+library modules. Differences for AndroidX repo:
+
+1. Module name must end with `-benchmark` in `settings.gradle`.
+2. You do not need to apply the benchmark plugin (it's pulled in automatically
+ from source)
+
+### I'm lazy and want to start quickly
+
+Start by copying one of the following projects:
+
+* [navigation-benchmark](https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-master-dev/navigation/benchmark/)
+* [recyclerview-benchmark](https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-master-dev/recyclerview/recyclerview-benchmark/)
+
+### Compose
+
+Compose builds the benchmark from source, so usage matches the rest of the
+AndroidX project. See existing Compose benchmark projects:
+
+* [Compose UI benchmarks](https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-master-dev/ui/integration-tests/benchmark/)
+* [Compose Runtime benchmarks](https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-master-dev/compose/compose-runtime/compose-runtime-benchmark/)
+
+## Profiling
+
+### Command Line
+
+The benchmark library supports capturing profiling information - sampled and
+method - from the command line. Here's an example which runs the
+`androidx.ui.benchmark.test.CheckboxesInRowsBenchmark#draw` method with
+`MethodSampling` profiling:
+
+```
+./gradlew compose:integ:bench:cC \
+ -P android.testInstrumentationRunnerArguments.androidx.benchmark.profiling.mode=MethodSampling \
+ -P android.testInstrumentationRunnerArguments.class=androidx.ui.benchmark.test.CheckboxesInRowsBenchmark#draw
+```
+
+The command output will tell you where to look for the file on your host
+machine:
+
+```
+04:33:49 I/Benchmark: Benchmark report files generated at
+/androidx-master-dev/out/ui/ui/integration-tests/benchmark/build/outputs/connected_android_test_additional_output
+```
+
+To inspect the captured trace, open the appropriate `*.trace` file in that
+directory with Android Studio, using `File > Open`.
+
+For more information on the `MethodSampling` and `MethodTracing` profiling
+modes, see the
+[Studio Profiler configuration docs](https://developer.android.com/studio/profile/cpu-profiler#configurations),
+specifically Java Sampled Profiling, and Java Method Tracing.
+
+![Sample flame chart](benchmarking_images/profiling_flame_chart.png "Sample flame chart")
+
+### Advanced: Simpleperf Method Sampling
+
+[Simpleperf](https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/)
+offers more accurate profiling for apps than standard method sampling, due to
+lower overhead (as well as C++ profiling support). Simpleperf support will be
+simplified and improved over time.
+
+[Simpleperf app profiling docs](https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/android_application_profiling.md).
+
+#### Device
+
+Get an API 28+ device (Or a rooted API 27 device). The rest of this section is
+about *why* those constraints exist, skip if not interested.
+
+Simpleperf has restrictions about where it can be used - Jetpack Benchmark will
+only support API 28+ for now, due to
+[platform/simpleperf constraints](https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/android_application_profiling.md#prepare-an-android-application)
+(see last subsection titled "If you want to profile Java code"). Summary is:
+
+- <=23 (M): Unsupported for Java code.
+
+- 24-25 (N): Requires compiled Java code. We haven't investigated support.
+
+- 26 (O): Requires compiled Java code, and wrapper script. We haven't
+ investigated support.
+
+- 27 (P): Can profile all Java code, but requires `userdebug`/rooted device
+
+- \>=28 (Q): Can profile all Java code, requires profileable (or
+ `userdebug`/rooted device)
+
+We aren't planning to support profiling debuggable APK builds, since they're
+misleading for profiling.
+
+#### Initial setup
+
+Currently, we rely on Python scripts built by the simpleperf team. We can
+eventually build this into the benchmark library / gradle plugin. Download the
+scripts from AOSP:
+
+```
+# copying to somewhere outside of the androidx repo
+git clone https://android.googlesource.com/platform/system/extras ~/simpleperf
+```
+
+Next configure your path to ensure the ADB that the scripts will use matches the
+androidx tools:
+
+```
+export PATH=$PATH:<path/to/androidx>/prebuilts/fullsdk-<linux or darwin>/platform-tools
+```
+
+Now, setup your device for simpleperf:
+
+```
+~/simpleperf/simpleperf/scripts/api_profiler.py prepare --max-sample-rate 10000000
+```
+
+#### Build and Run, Option 1: Studio (slightly recommended)
+
+Running from Studio is simpler, since you don't have to manually install and run
+the APKs, avoiding Gradle.
+
+Add the following to the benchmark module's build.gradle:
+
+```
+android {
+ defaultConfig {
+ // DO NOT COMMIT!!
+ testInstrumentationRunnerArgument 'androidx.benchmark.profiling.mode', 'MethodSamplingSimpleperf'
+ // Optional: Control freq / duration.
+ testInstrumentationRunnerArgument 'androidx.benchmark.profiler.sampleFrequency', '1000000'
+ testInstrumentationRunnerArgument 'androidx.benchmark.profiler.sampleDurationSeconds', '5'
+ }
+}
+```
+
+And run the test or tests you'd like to measure from within Studio.
+
+#### Build and Run, Option 2: Command Line
+
+**Note - this will be significantly simplified in the future**
+
+Since we're not using AGP to pull the files yet, we can't invoke the benchmark
+through Gradle, because Gradle uninstalls after each test run. Instead, let's
+just build and run manually:
+
+```
+./gradlew compose:integration-tests:benchmark:assembleReleaseAndroidTest
+
+adb install -r ../../../out/ui/compose/integration-tests/benchmark/build/outputs/apk/androidTest/release/benchmark-release-androidTest.apk
+
+# run the test (can copy this line from Studio console, when running a benchmark)
+adb shell am instrument -w -m --no-window-animation -e androidx.benchmark.profiling.mode MethodSamplingSimpleperf -e debug false -e class 'androidx.ui.benchmark.test.CheckboxesInRowsBenchmark#toggleCheckbox_draw' androidx.ui.benchmark.test/androidx.benchmark.junit4.AndroidBenchmarkRunner
+```
+
+#### Pull and open the trace
+
+```
+# move the files to host
+# (Note: removes files from device)
+~/simpleperf/simpleperf/scripts/api_profiler.py collect -p androidx.ui.benchmark.test -o ~/simpleperf/results
+
+# create/open the HTML report
+~/simpleperf/simpleperf/scripts/report_html.py -i ~/simpleperf/results/CheckboxesInRowsBenchmark_toggleCheckbox_draw\[1\].data
+```
+
+### Advanced: Studio Profiling
+
+Profiling for allocations and simpleperf profiling requires Studio to capture.
+
+Studio profiling tools require `debuggable=true`. First, temporarily override it
+in your benchmark's `androidTest/AndroidManifest.xml`.
+
+Next choose which profiling you want to do: Allocation, or Sampled (SimplePerf)
+
+`ConnectedAllocation` will help you measure the allocations in a single run of a
+benchmark loop, after warmup.
+
+`ConnectedSampled` will help you capture sampled profiling, but with the more
+detailed / accurate Simpleperf sampling.
+
+Set the profiling type in your benchmark module's `build.gradle`:
+
+```
+android {
+ defaultConfig {
+ // Local only, don't commit this!
+ testInstrumentationRunnerArgument 'androidx.benchmark.profiling.mode', 'ConnectedAllocation'
+ }
+}
+```
+
+Run `File > Sync Project with Gradle Files`, or sync if Studio asks you. Now any
+benchmark runs in that project will permit debuggable, and pause before and
+after the test, to allow you to connect a profiler and start recording, and then
+stop recording.
+
+#### Running and Profiling
+
+After the benchmark test starts, you have about 20 seconds to connect the
+profiler:
+
+1. Click the profiler tab at the bottom
+1. Click the plus button in the top left, `<device name>`, `<process name>`
+1. Next step depends on which you intend to capture
+
+#### Allocations
+
+Click the memory section, and right click the window, and select `Record
+allocations`. Approximately 20 seconds later, right click again and select `Stop
+recording`.
diff --git a/docs/benchmarking_images/filter_build.png b/docs/benchmarking_images/filter_build.png
new file mode 100644
index 0000000..f4d15d4
--- /dev/null
+++ b/docs/benchmarking_images/filter_build.png
Binary files differ
diff --git a/docs/benchmarking_images/filter_initial.png b/docs/benchmarking_images/filter_initial.png
new file mode 100644
index 0000000..61bdc30
--- /dev/null
+++ b/docs/benchmarking_images/filter_initial.png
Binary files differ
diff --git a/docs/benchmarking_images/filter_test.png b/docs/benchmarking_images/filter_test.png
new file mode 100644
index 0000000..9a8a0ee
--- /dev/null
+++ b/docs/benchmarking_images/filter_test.png
Binary files differ
diff --git a/docs/benchmarking_images/profiling_flame_chart.png b/docs/benchmarking_images/profiling_flame_chart.png
new file mode 100644
index 0000000..33cd76f
--- /dev/null
+++ b/docs/benchmarking_images/profiling_flame_chart.png
Binary files differ
diff --git a/docs/benchmarking_images/result_plot.png b/docs/benchmarking_images/result_plot.png
new file mode 100644
index 0000000..d0b878e
--- /dev/null
+++ b/docs/benchmarking_images/result_plot.png
Binary files differ
diff --git a/docs/benchmarking_images/triage_bug.png b/docs/benchmarking_images/triage_bug.png
new file mode 100644
index 0000000..816122c
--- /dev/null
+++ b/docs/benchmarking_images/triage_bug.png
Binary files differ
diff --git a/docs/benchmarking_images/triage_cl_list.png b/docs/benchmarking_images/triage_cl_list.png
new file mode 100644
index 0000000..d65a7e2
--- /dev/null
+++ b/docs/benchmarking_images/triage_cl_list.png
Binary files differ
diff --git a/docs/benchmarking_images/triage_complete.png b/docs/benchmarking_images/triage_complete.png
new file mode 100644
index 0000000..3a04763
--- /dev/null
+++ b/docs/benchmarking_images/triage_complete.png
Binary files differ
diff --git a/docs/benchmarking_images/triage_graph.png b/docs/benchmarking_images/triage_graph.png
new file mode 100644
index 0000000..90defbc
--- /dev/null
+++ b/docs/benchmarking_images/triage_graph.png
Binary files differ
diff --git a/docs/benchmarking_images/triage_initial.png b/docs/benchmarking_images/triage_initial.png
new file mode 100644
index 0000000..ea0d033
--- /dev/null
+++ b/docs/benchmarking_images/triage_initial.png
Binary files differ
diff --git a/docs/benchmarking_images/triage_regression.png b/docs/benchmarking_images/triage_regression.png
new file mode 100644
index 0000000..bed4b5f
--- /dev/null
+++ b/docs/benchmarking_images/triage_regression.png
Binary files differ
diff --git a/docs/benchmarking_images/triage_word_cloud.png b/docs/benchmarking_images/triage_word_cloud.png
new file mode 100644
index 0000000..35dbe43
--- /dev/null
+++ b/docs/benchmarking_images/triage_word_cloud.png
Binary files differ
diff --git a/docs/branching.md b/docs/branching.md
new file mode 100644
index 0000000..8b5cd06
--- /dev/null
+++ b/docs/branching.md
@@ -0,0 +1,40 @@
+# AndroidX Branch Workflow
+
+[TOC]
+
+## Single Development Branch [androidx-master-dev]
+
+All feature development occurs in the public AndroidX master dev branch of the
+Android Open Source Project: `androidx-master-dev`. This branch serves as the
+central location and source of truth for all AndroidX library source code. All
+alpha and beta version development, builds, and releases will be done ONLY in
+this branch.
+
+## Release Branches [androidx-\<feature\>-release]
+
+When a library updates to rc (release-candidate) or stable, that library version
+will be snapped over to that library’s release branch. If that release branch
+doesn’t exist, then a release branch will be created for that library, snapped
+from androidx-master-dev at the commit that changed the library to an rc or
+stable version.
+
+Release branches have the following properties:
+
+* A release branch will contain rc or stable versions of libraries.
+* Release branches are internal branches.
+* Release branches can **ONLY** be changed through
+ cherry-picks
+* Bug-fixes and updates to that rc or stable version will need to be
+ individually cherry-picked
+* No alpha or beta versions will exist in a release branch.
+* Toolchain and other library wide changes to androidx-master-dev will be
+ synced to each release branch.
+* Release branches will have the naming format
+ `androidx-<feature-name>-release`
+* Release branches will be re-snapped from `androidx-master-dev` for each new
+ minor version release (for example, releasing 2.2.0-rc01 after 2.1.0)
+
+## Platform Developement and AndroidX [androidx-platform-dev]
+
+Platform specific development is done using our INTERNAL platform development
+branch `androidx-platform-dev`.
diff --git a/docs/branching_images/cs_branch_switcher.png b/docs/branching_images/cs_branch_switcher.png
new file mode 100644
index 0000000..660a877
--- /dev/null
+++ b/docs/branching_images/cs_branch_switcher.png
Binary files differ
diff --git a/docs/branching_images/cs_change.png b/docs/branching_images/cs_change.png
new file mode 100644
index 0000000..15c7475
--- /dev/null
+++ b/docs/branching_images/cs_change.png
Binary files differ
diff --git a/docs/branching_images/cs_editor.png b/docs/branching_images/cs_editor.png
new file mode 100644
index 0000000..72ec1ee
--- /dev/null
+++ b/docs/branching_images/cs_editor.png
Binary files differ
diff --git a/docs/branching_images/jetpack_branch_workflow.png b/docs/branching_images/jetpack_branch_workflow.png
new file mode 100644
index 0000000..ca4b094
--- /dev/null
+++ b/docs/branching_images/jetpack_branch_workflow.png
Binary files differ
diff --git a/docs/branching_images/release_branch_diagram.png b/docs/branching_images/release_branch_diagram.png
new file mode 100644
index 0000000..7b54025
--- /dev/null
+++ b/docs/branching_images/release_branch_diagram.png
Binary files differ
diff --git a/docs/do_not_mock.md b/docs/do_not_mock.md
new file mode 100644
index 0000000..d614001
--- /dev/null
+++ b/docs/do_not_mock.md
@@ -0,0 +1,123 @@
+# Do Not Mock, AndroidX
+
+All APIs created in AndroidX **must have a testing story**: how developers
+should write tests for their code that relies on a library, this story should
+not be "use mockito to mock class `Foo`". Your goal as API owner is to **create
+better alternatives** to mocking.
+
+## Why can't I suggest mocks as testing strategy?
+
+Frequently mocks don't follow guarantees outlined in the API they mock. That
+leads to:
+
+* Significant difference in the behavior that diminishes test value.
+* Brittle tests, that make hard to evolve both apps and libraries, because new
+ code may start to rely on the guarantees broken in a mock. Let's take a look
+ at a simplified example. So, let's say you mocked a bundle and getString in
+ it:
+
+ ```java
+ Bundle mock = mock(Bundle.class);
+ when(mock.getString("key")).thenReturn("result");
+ ```
+
+ But you don't mock it to simply call `getString()` in your test. A goal is
+ not to test a mock, the goal is always to test your app code, so your app
+ code always interacts with a mock in some way:
+
+ ```java
+ Bundle bundle = mock(Bundle.class);
+ when(mock.getString("key")).thenReturn("result");
+ mycomponent.consume(bundle)
+ ```
+
+ Originally the test worked fine, but over time `component.consume` is
+ evolving, and, for example, it may start to call `containsKey` on the given
+ bundle. But our test passes a mock that don't expect such call and, boom,
+ test is broken. However, component code is completely valid and has nothing
+ to do with the broken test. We observed a lot of issues like that during
+ updates of android SDK and AndroidX libraries to newer versions internally
+ at google. Suggesting to mock our own components is shooting ourselves in
+ the foot, it will make adoption of newer version of libraries even slower.
+
+* Messy tests. It always starts with simple mock with one method, but then
+ this mock grows with the project, and as a result test code has sub-optimal
+ half-baked class implementation of on top of the mock.
+
+## But it is ok to mock interfaces, right?
+
+It depends. There are interfaces that don't imply any behavior guarantees and
+they are ok to be mocked. However, **not all** interfaces are like that: for
+example, `Map` is an interface but it has a lot of contracts required from
+correct implementation. Examples of interfaces that are ok to mock are callback
+interfaces in general, for example: `View.OnClickListener`, `Runnable`.
+
+## What about spying?
+
+Spying on these classes is banned as well - mockito spies permit stubbing of
+methods just like mocks do, and interaction verification is brittle and
+unnecessary for these classes. Rather than verifying an interaction with a
+class, developers should observe the result of an interaction - the effect of a
+task submitted to an `Executor`, or the presence of a fragment added to your
+layout. If an API in your library misses a way to have such checks, you should
+add methods to do that. If you think it is dangerous to open such methods in the
+main surface of your library, consult with
+[API council](https://sites.google.com/corp/google.com/android-api-council), it
+may have seen similar patterns before. For example, one of the possible ways to
+resolve such issue can be adding test artifact with special capabilities. So
+`fragment-testing` module was created to drive lifecycle of Fragment and ease
+interaction with fragments in tests.
+
+## Avoid mockito in your own tests.
+
+One of the things that would help you to identify if your library is testable
+without mockito is not using mockito yourself. Yes, historically we heavily
+relied on mockito ourselves and old tests are not rewritten, but new tests
+shouldn't follow up that and should take as an example good citizens, for
+example, `-ktx` modules. These modules don't rely on mockito and have concise
+expressive tests.
+
+One of the popular and legit patterns for mockito usage were tests that verify
+that a simple callback-like interface receives correct parameters.
+
+```java
+class MyApi {
+ interface Callback {
+ void onFoo(Value value);
+ }
+ void foo() { … }
+ void registerFooCallback(Callback callback) {...}
+}
+```
+
+In api like the one above, in java 7 tests for value received in `Callback`
+tended to become very wordy without mockito. But now in your tests you can use
+Kotlin and test will be as short as with mockito:
+
+```kotlin
+fun test() {
+ var receivedValue = null
+ myApi.registerCallback { value -> receivedValue = value }
+ myApi.foo()
+ // verify receivedValue
+}
+```
+
+## Don't compromise in API to enable mockito
+
+Mockito on android
+[had an issue](https://github.com/mockito/mockito/issues/1173) with mocking
+final classes. Moreover, internally at google this feature is disabled even for
+non-android code. So you may hear complaints that some of your classes are not
+mockable, however **It is not a reason for open up a class for extension**. What
+you should instead is verify that is possible to write the same test without
+mocking, if not, again you should **provide better alternative in your API**.
+
+## How do I approach testing story for my API?
+
+Best way is to step into developer's shoes and write a sample app that is a
+showcase for your API, then go to the next step - test that code also. If you
+are able to implement tests for your demo app, then users of your API will also
+be able to implement tests for functionalities where your API is also used.
+
+## ~~Use @DoNotMock on most of your APIs ~~(Not available yet)
diff --git a/docs/faq.md b/docs/faq.md
new file mode 100644
index 0000000..786cc23
--- /dev/null
+++ b/docs/faq.md
@@ -0,0 +1,171 @@
+# FAQ
+
+[TOC]
+
+## General FAQ
+
+### What is AndroidX?
+
+The Android Extension (AndroidX) Libraries provide functionality that extends
+the capabilities of the Android platform. These libraries, which ship separately
+from the Android OS, focus on improving the experience of developing apps
+through broad OS- and device-level compatibility, high-level abstractions to
+simplify and unify platform features, and other new features that target
+developer pain points. To find out more about AndroidX, see the public
+documentation on developer.android.com.
+
+### Why did we move to AndroidX?
+
+Please read our
+[blog post](https://android-developers.googleblog.com/2018/05/hello-world-androidx.html)
+about our migration to AndroidX.
+
+### What happened to the Support Library?
+
+As part of the Jetpack effort to improve developer experience on Android, the
+Support Library team undertook a massive refactoring project. Over the course of
+2017 and 2018, we streamlined and enforced consistency in our packaging,
+developed new policies around vesioning and releasing, and developed tools to
+make it easy for developers to migrate.
+
+### Will there be any more updates to Support Library?
+
+No, Revision 28.0.0 of the Support Library, which launched as stable in
+September 2018, was the last feature release in the android.support package.
+There will be no further releases under Support Library packaging.
+
+### How is AndroidX related to Jetpack?
+
+They are the same thing! In a sentence, AndroidX is the packaging and
+internally-facing development project for all components in Jetpack. Jetpack is
+the external branding for libraries within AndroidX.
+
+In more detail, Jetpack is the external branding for the set of components,
+tools, and guidance that improve the developer experience on Android. AndroidX
+is the open-source development project that defines the workflow, versioning,
+and release policies for ALL libraries included in Jetpack. All libraries within
+the androidx Java package follow a consistent set of API design guidelines,
+conform to SemVer and alpha/beta revision cycles, and use the Android issue
+tracker for bugs and feature requests.
+
+### What AndroidX library versions have been officially released?
+
+You can see all publicly released versions on the interactive
+[Google Maven page](https://dl.google.com/dl/android/maven2/index.html).
+
+### How do I jetify something?
+
+The Standalone Jetifier documentation and download link can be found
+[here](https://developer.android.com/studio/command-line/jetifier), under the
+Android Studio DAC.
+
+### How do I update my library version?
+
+See the steps specified on the version page
+[here](versioning.md#how-to-update-your-version).
+
+### How do I test my change in a separate Android Studio project?
+
+If you're working on a new feature or bug fix in AndroidX, you may want to test
+your changes against another project to verify that the change makes sense in a
+real-world context or that a bug's specific repro case has been fixed.
+
+If you need to be absolutely sure that your test will exactly emulate the
+developer's experience, you can repeatedly build the AndroidX archive and
+rebuild your application. In this case, you will need to create a local build of
+AndroidX's local Maven repository artifact and install it in your Android SDK
+path.
+
+First, use the `createArchive` Gradle task to generate the local Maven
+repository artifact:
+
+```shell
+# Creates <path-to-checkout>/out/dist/sdk-repo-linux-m2repository-##.zip
+./gradlew createArchive
+```
+
+Next, take the ZIP output from this task and extract the contents to the Android
+SDK path that you are using for your alternate (non-AndroidX) version of Android
+Studio. For example, you may be using `~/Android/SDK/extras` if you are using
+the default Android Studio SDK for app development or
+`prebuilts/fullsdk-linux/extras` if you are using fullsdk for platform
+development.
+
+```shell
+# Creates or overwrites android/m2repository
+cd <path-to-sdk>/extras
+unzip <path-to-checkout>/out/dist/top-of-tree-m2repository-##.zip
+```
+
+Finally, in the dependencies section of your standalone project's `build.gradle`
+file, add or update the `compile` entries to reflect the AndroidX modules that
+you would like to test:
+
+```
+dependencies {
+ ...
+ compile "com.android.support:appcompat-v7:26.0.0-SNAPSHOT"
+}
+```
+
+## Version FAQ {#version}
+
+### How are changes in dependency versions propagated?
+
+If you declare `api(project(":depGroupId"))` in your `build.gradle`, then the
+version change will occur automatically. While convienent, be intentional when
+doing so because this causes your library to have a direct dependency on the
+version in development.
+
+If you declare `api("androidx.depGroupId:depArtifactId:1.0.0")`, then the
+version change will need to be done manually and intentionally. This is
+considered best practice.
+
+### How does a library begin work on a new Minor version?
+
+Set the version to the next minor version, as an alpha.
+
+### How does a library ship an API reference documentation bugfix?
+
+Developers obtain API reference documentation from two sources -- HTML docs on
+[d.android.com](https://d.android.com), which are generated from library release
+artifacts, and Javadoc from source JARs on Google Maven.
+
+As a result, documentation bug fixes should be held with other fixes until they
+can go through a normal release cycle. Critical (e.g. P0) documentation issues
+**may** result in a [bugfix](loaf.md#bugfix) release independent of other fixes.
+
+### When does an alpha ship?
+
+For public releases, an alpha ships when the library lead believes it is ready.
+Generally, these occur during the batched bi-weekly (every 2 weeks) release
+because all tip-of-tree dependencies will need to be released too.
+
+### Are there restrictions on when or how often an alpha can ship?
+
+Nope.
+
+### Can Alpha work (ex. for the next Minor release) occur in the primary development branch during Beta API lockdown?
+
+No. This is by design. Focus should be spent on improving the Beta version and
+adding documentation/samples/blog posts for usage!
+
+### Is there an API freeze window between Alpha and Beta while API surface is reviewed and tests are added, but before the Beta is released?
+
+Yes. If any new APIs are added in this window, the beta release will be blocked
+until API review is complete and addressed.
+
+### How often can a Beta release?
+
+As often as needed, however, releases outside of the bi-weekly (every 2 weeks)
+release will need to get approval from the TPM (nickanthony@).
+
+### What are the requirements for moving from Alpha to Beta?
+
+See the [Beta section of Versioning guidelines](versioning.md?#beta) for
+pre-release cycle transition requirements.
+
+### What are the requirements for a Beta launch?
+
+See the [Beta section of Versioning guidelines](versioning.md?#beta) for
+pre-release cycle transition requirements.
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..4a29386
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,47 @@
+# What is Jetpack?
+
+## Jetpack Ethos
+
+To create recommended components, tools, and guidance that makes it quick and
+easy to build great Android apps, including pieces both from Google and from
+trusted OSS sources.
+
+## Team Mission
+
+To improve the Android developer experience by providing architectural guidance,
+addressing common pain points, and simplifying the app development process
+through broad compatibility across Android versions and elimination of
+boilerplate code so developers can focus on what makes their app special.
+
+## What is `androidx`?
+
+Artifacts within the `androidx` package comprise the libraries of
+[Android Jetpack](https://developer.android.com/jetpack).
+
+Libraries in the `androidx` package provide functionality that extends the
+capabilities of the Android platform. These libraries, which ship separately
+from the Android OS, focus on improving the experience of developing apps
+through broad OS- and device-level compatibility, high-level abstractions to
+simplify and unify platform features, and other new features that target
+developer pain points.
+
+## What happened to the Support Library?
+
+As part of the Jetpack project to improve developer experience on Android, the
+Support Library team undertook a massive refactoring project. Over the course of
+2017 and 2018, we streamlined and enforced consistency in our packaging,
+developed new policies around versioning and release, and developed tools to
+make it easy for developers to migrate.
+
+Revision 28.0.0 of the Support Library, which launched as stable in September
+2018, was the last feature release in the `android.support` package. There will
+be no further releases under Support Library packaging.
+
+## Quick links
+
+### Filing an issue
+
+Have a bug or feature request? Please check our
+[public Issue Tracker component](http://issuetracker.google.com/issues/new?component=192731&template=842428)
+for duplicates first, then file against the appropriate sub-component according
+to the library package or infrastructure system.
diff --git a/docs/issue_tracking.md b/docs/issue_tracking.md
new file mode 100644
index 0000000..3413ab2
--- /dev/null
+++ b/docs/issue_tracking.md
@@ -0,0 +1,101 @@
+# Issue Lifecycle and Reporting Guidelines
+
+[TOC]
+
+## Issue tracker
+
+The public-facing issue tracker URL is
+[issuetracker.google.com](https://issuetracker.google.com). If you visit this
+URL from a corp account, it will immediately redirect you to the internal-facing
+issue tracker URL. Make sure that any links you paste publicly have the correct
+public-facing URL.
+
+The top-level Jetpack component is
+[`Android Public Tracker > App Development > Jetpack (androidx)`](https://issuetracker.google.com/components/192731/manage#basic).
+
+## Reporting guidelines
+
+Issue Tracker isn't a developer support forum. For support information, consider
+[StackOverflow](http://stackoverflow.com).
+
+Support for Google apps is through
+[Google's support site](http://support.google.com/). Support for third-party
+apps is provided by the app's developer, for example through the contact
+information provided on Google Play.
+
+1. Search for your bug to see if anyone has already reported it. Don't forget
+ to search for all issues, not just open ones, as your issue might already
+ have been reported and closed. To help you find the most popular results,
+ sort the result by number of stars.
+
+1. If you find your issue and it's important to you, star it! The number of
+ stars on a bug helps us know which bugs are most important to fix.
+
+1. If no one has reported your bug, file the bug. First, browse for the correct
+ component -- typically this has a 1:1 correspondence with Maven group ID --
+ and fill out the provided template.
+
+1. Include as much information in the bug as you can, following the
+ instructions for the bug queue that you're targeting. A bug that simply says
+ something isn't working doesn't help much, and will probably be closed
+ without any action. The amount of detail that you provide, such as a minimal
+ sample project, log files, repro steps, and even a patch set, helps us
+ address your issue.
+
+## Status definitions
+
+| Status | Description |
+| -------- | ----------------------------------------------------------------- |
+| New | The default for public bugs. Waiting for someone to validate, |
+: : reproduce, or otherwise confirm that this is actionable. :
+| Assigned | Pending action from the assignee. May be reassigned. |
+| Accepted | Actively being worked on by the assignee. Do not reassign. |
+| Fixed | Fixed in the development branch. Do not re-open unless the fix is |
+: : reverted. :
+| WontFix | Covers all the reasons we chose to close the issue without taking |
+: : action (can't repro, working as intended, obsolete). :
+
+## Priority criteria and SLOs
+
+| Priority | Criteria | Resolution time |
+| -------- | ------------------------------ | ------------------------------ |
+| P0 | This priority is limited to | Less than 1 day. Don't go home |
+: : service outages, blocking : until this is fixed. :
+: : issues, or other types of work : :
+: : stoppage such as issues on the : :
+: : Platform chase list requiring : :
+: : immediate attention. : :
+| P1 | This priority is limited to | Within the next 7 days |
+: : work that requires rapid : :
+: : resolution, but can be dealt : :
+: : with in a slightly longer time : :
+: : window than P0. : :
+| P2 | Won't ship without this. | Within the current release |
+| P3 | Would rather not ship without | Less than 365 days |
+: : this, but would decide case by : :
+: : case. : :
+| P4 | Issue has not yet been | N/A (must triage in under 14 |
+: : prioritized (default as of Feb : days) :
+: : 2013). : :
+
+## Issue lifecycle
+
+1. When an issue is reported, it is set to **Assigned** status for default
+ assignee (typically the [library owner](owners.md)) with a priority of
+ **P4**.
+ * Some components have an empty default assignee and will be manually
+ assigned by the [triage cop](triage_cop.md)
+1. Once an issue has been triaged by the assignee, its priority will be raised
+ from **P4** according to severity.
+1. The issue may still be reassigned at this point.
+ [Bug bounty](onboarding.md#bug-bounty) issues are likely to change
+ assignees.
+1. A status of **Accepted** means the assignee is actively working on the
+ issue.
+1. A status of **Fixed** means that the issue has been resolved in the
+ development branch. Please note that it may take some time for the fix to
+ propagate into various release channels (internal repositories, Google
+ Maven, etc.). **Do not** re-open an issue because the fix has not yet
+ propagated into a specific release channel. **Do not** re-open an issue that
+ has been fixed unless the fix was reverted or the exact reported issue is
+ still occurring.
diff --git a/docs/LINT.md b/docs/lint_guide.md
similarity index 86%
rename from docs/LINT.md
rename to docs/lint_guide.md
index 5916b6d..5e40600 100644
--- a/docs/LINT.md
+++ b/docs/lint_guide.md
@@ -1,5 +1,7 @@
# Adding custom Lint checks
+[TOC]
+
## Getting started
Lint is a static analysis tool that checks Android project source files. Lint
@@ -47,8 +49,8 @@
}
dependencies {
- // compileOnly because we use lintChecks and it doesn't allow other types of deps
- // this ugly hack exists because of b/63873667
+ // compileOnly because lint runtime is provided when checks are run
+ // Use latest lint for running from IDE to make sure checks always run
if (rootProject.hasProperty("android.injected.invoked.from.ide")) {
compileOnly LINT_API_LATEST
} else {
@@ -81,7 +83,7 @@
Your new module will need to have a registry that contains a list of all of the
checks to be performed on the library. There is an
-[`IssueRegistry`](https://cs.android.com/android/platform/superproject/+/master:tools/base/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/IssueRegistry.java)
+[`IssueRegistry`](https://cs.android.com/android/platform/superproject/+/master:tools/base/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/IssueRegistry.java;l=47)
class provided by the tools team. Extend this class into your own
`IssueRegistry` class, and provide it with the issues in the module.
@@ -103,7 +105,7 @@
`CURRENT_API` is defined by the Lint API version against which your project is
compiled, as defined in the module's `build.gradle` file. Jetpack Lint modules
should compile using Lint API version 3.3 defined in
-[Dependencies.kt](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt;l=84).
+[Dependencies.kt](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt;l=176).
We guarantee that our Lint checks work with versions 3.3-3.6 by running our
tests with both versions 3.3 and 3.6. For newer versions of Android Studio (and
@@ -268,7 +270,7 @@
These are Lint checks that will apply to source code files -- primarily Java and
Kotlin, but can also be used for other similar file types. All code detectors
that analyze Java or Kotlin files should implement the
-[SourceCodeScanner](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/SourceCodeScanner.kt).
+[SourceCodeScanner](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/SourceCodeScanner.kt).
### API surface
@@ -401,7 +403,7 @@
These are Lint rules that will apply to resource files including `anim`,
`layout`, `values`, etc. Lint rules being applied to resource files should
extend
-[`ResourceXmlDetector`](https://cs.android.com/android/platform/superproject/+/master:tools/base/lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/ResourceXmlDetector.java).
+[`ResourceXmlDetector`](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/ResourceXmlDetector.java).
The `Detector` must define the issue it is going to detect, most commonly as a
static variable of the class.
@@ -436,7 +438,7 @@
#### appliesTo
This determines the
-[ResourceFolderType](https://cs.android.com/android/platform/superproject/+/master:tools/base/layoutlib-api/src/main/java/com/android/resources/ResourceFolderType.java)
+[ResourceFolderType](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:layoutlib-api/src/main/java/com/android/resources/ResourceFolderType.java)
that the check will run against.
```kotlin
@@ -503,7 +505,7 @@
```
Next, you must test the `Detector` class. The Tools team provides a
-[`LintDetectorTest`](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/LintDetectorTest.java)
+[`LintDetectorTest`](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/LintDetectorTest.java)
class that should be extended. Override `getDetector()` to return an instance of
the `Detector` class:
@@ -517,13 +519,13 @@
getIssues(): MutableList<Issue> = mutableListOf(MyLibraryDetector.ISSUE)
```
-[`LintDetectorTest`](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/LintDetectorTest.java)
+[`LintDetectorTest`](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/LintDetectorTest.java)
provides a `lint()` method that returns a
-[`TestLintTask`](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/TestLintTask.java).
+[`TestLintTask`](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/TestLintTask.java).
`TestLintTask` is a builder class for setting up lint tests. Call the `files()`
method and provide an `.xml` test file, along with a file stub. After completing
the set up, call `run()` which returns a
-[`TestLintResult`](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/TestLintResult.kt).
+[`TestLintResult`](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/TestLintResult.kt).
`TestLintResult` provides methods for checking the outcome of the provided
`TestLintTask`. `ExpectClean()` means the output is expected to be clean because
the lint rule was followed. `Expect()` takes a string literal of the expected
@@ -536,13 +538,13 @@
## Android manifest detector
Lint checks targeting `AndroidManifest.xml` files should implement the
-[XmlScanner](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/XmlScanner.kt)
+[XmlScanner](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/XmlScanner.kt)
and define target scope in issues as `Scope.MANIFEST`
## Gradle detector
Lint checks targeting Gradle configuration files should implement the
-[GradleScanner](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/GradleScanner.kt)
+[GradleScanner](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/GradleScanner.kt)
and define target scope in issues as `Scope.GRADLE_SCOPE`
### API surface
@@ -599,7 +601,7 @@
Sometimes it is necessary to implement multiple different scanners in a Lint
detector. For example, the
-[Unused Resource](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/UnusedResourceDetector.java)
+[Unused Resource](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/UnusedResourceDetector.java)
Lint check implements an XML and SourceCode Scanner in order to determine if
resources defined in XML files are ever references in the Java/Kotlin source
code.
@@ -621,16 +623,16 @@
## Useful classes/packages
-### [`SdkConstants`](https://cs.android.com/android/platform/superproject/+/master:tools/base/common/src/main/java/com/android/SdkConstants.java;l=38?q=SdkCon)
+### [`SdkConstants`](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:common/src/main/java/com/android/SdkConstants.java)
Contains most of the canonical names for android core library classes, as well
as XML tag names.
## Helpful links
-[Studio Lint Rules](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks)
+[Studio Lint Rules](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/)
-[Lint Detectors and Scanners Source Code](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api)
+[Lint Detectors and Scanners Source Code](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/)
[Creating Custom Link Checks (external)](https://twitter.com/alexjlockwood/status/1176675045281693696)
@@ -644,4 +646,4 @@
[ADS 19 Presentation by Alan & Rahul](https://www.youtube.com/watch?v=jCmJWOkjbM0)
-[META-INF vs Manifest](https://groups.google.com/forum/#!msg/lint-dev/z3NYazgEIFQ/hbXDMYp5AwAJ)
\ No newline at end of file
+[META-INF vs Manifest](https://groups.google.com/forum/#!msg/lint-dev/z3NYazgEIFQ/hbXDMYp5AwAJ)
diff --git a/docs/manual_prebuilts_dance.md b/docs/manual_prebuilts_dance.md
new file mode 100644
index 0000000..d06e86f
--- /dev/null
+++ b/docs/manual_prebuilts_dance.md
@@ -0,0 +1,22 @@
+# The Manual Prebuilts Dance™
+
+NOTE There is also a [script](releasing.md#the-prebuilts-dance™) that automates
+this step.
+
+Public-facing Jetpack library docs are built from prebuilts to reconcile our
+monolithic docs update process with our independently-versioned library release
+process.
+
+Submit the following changes in the same Gerrit topic so that they merge in the
+same build ID:
+
+1. Commit your release artifact to the AndroidX AOSP checkout's local Maven
+ repository under `prebuilts/androidx/internal`.
+
+2. Update the version for your library in the public docs configuration
+ ([docs-public/build.gradle](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:docs-public/build.gradle)).
+ If this is the first time that your library is being published, you will
+ need to add a new entry.
+
+Once both changes are, make sure to note the build ID where they landed. You
+will need to put this in your release request bug for Docs team.
diff --git a/docs/onboarding.md b/docs/onboarding.md
new file mode 100644
index 0000000..5bd331b
--- /dev/null
+++ b/docs/onboarding.md
@@ -0,0 +1,790 @@
+# Getting started
+
+[TOC]
+
+This page describes how to set up your workstation to check out source code,
+make simple changes in Android Studio, and upload commits to Gerrit for review.
+
+This page does **not** cover best practices for the content of changes. Please
+see [Life of a Jetpack Feature](loaf.md) for details on developing and releasing
+a library, [API Guidelines](api_guidelines.md) for best practices regarding
+public APIs, or [Policies and Processes](policies.md) for an overview of the
+constraints placed on changes.
+
+## Workstation setup {#setup}
+
+You will need to install the `repo` tool, which is used for Git branch and
+commit management. If you want to learn more about `repo`, see the
+[Repo Command Reference](https://source.android.com/setup/develop/repo).
+
+### Linux and MacOS {#setup-linux-mac}
+
+First, download `repo` using `curl`.
+
+```shell
+test -d ~/bin || mkdir ~/bin
+curl https://storage.googleapis.com/git-repo-downloads/repo \
+ > ~/bin/repo && chmod 700 ~/bin/repo
+```
+
+Then, modify `~/.bash_profile` (if using `bash`) to ensure you can find local
+binaries from the command line.
+
+```shell
+export PATH=~/bin:$PATH
+```
+
+You will need to either start a new terminal session or run `source
+~/.bash_profile` to pick up the new path.
+
+If you encounter an SSL `CERTIFICATE_VERIFY_FAILED` error or warning about
+Python 2 being no longer supported, you will need to install Python 3 and alias
+your `repo` command to run with `python3`.
+
+```shell {.bad}
+repo: warning: Python 2 is no longer supported; Please upgrade to Python 3.6+.
+```
+
+```shell {.bad}
+Downloading Repo source from https://gerrit.googlesource.com/git-repo
+fatal: Cannot get https://gerrit.googlesource.com/git-repo/clone.bundle
+fatal: error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:777)
+```
+
+First, install Python 3 from the [official website](https://www.python.org).
+Please read the "Important Information" displayed during installation for
+information about SSL/TLS certificate validation and the running the "Install
+Certificates.command".
+
+Next, open your `~/.bash_profile` and add the following lines to wrap the `repo`
+command:
+
+```shell
+# Force repo to run with Python3
+function repo() {
+ command python3 "$(which repo)" $@
+}
+```
+
+### Windows {#setup-win}
+
+Sorry, Windows is not a supported platform for AndroidX development.
+
+## Set up access control {#access}
+
+### Authenticate to AOSP Gerrit {#access-gerrit}
+
+Before you can upload changes, you will need to associate your Google
+credentials with the AOSP Gerrit code review system by signing in to
+[android-review.googlesource.com](https://android-review.googlesource.com) at
+least once using the account you will use to submit patches.
+
+Next, you will need to
+[set up authentication](https://android-review.googlesource.com/new-password).
+This will give you a shell command to update your local Git cookies, which will
+allow you to upload changes.
+
+Finally, you will need to accept the
+[CLA for new contributors](https://android-review.googlesource.com/settings/new-agreement).
+
+## Check out the source {#source}
+
+Like ChromeOS, Chromium, and the Android build system, we develop in the open as
+much as possible. All feature development occurs in the public
+[androidx-master-dev](https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev)
+branch of the Android Open Source Project.
+
+As of 2020/03/20, you will need about 38 GB for a fully-built checkout.
+
+### Synchronize the branch {#source-checkout}
+
+Use the following `repo` commands to check out your branch.
+
+#### Public master development branch {#source-checkout-master}
+
+All development should occur in this branch unless otherwise specified by the
+AndroidX Core team.
+
+The following command will check out the public master development branch:
+
+```shell
+mkdir androidx-master-dev && cd androidx-master-dev
+repo init -u https://android.googlesource.com/platform/manifest \
+ -b androidx-master-dev --partial-clone --clone-filter=blob:limit=10M
+repo sync -c -j8
+```
+
+NOTE On MacOS, if you receive an SSL error like `SSL: CERTIFICATE_VERIFY_FAILED`
+you may need to install Python3 and boot strap the SSL certificates in the
+included version of pip. You can execute `Install Certificates.command` under
+`/Applications/Python 3.6/` to do so.
+
+### Increase Git rename limit {#source-config}
+
+To ensure `git` can detect diffs and renames across significant changes (namely,
+the `androidx.*` package rename), we recommend that you set the following `git
+config` properties:
+
+```shell
+git config --global merge.renameLimit 999999
+git config --global diff.renameLimit 999999
+```
+
+## Explore source code from a browser {#code-search}
+
+`androidx-master-dev` has a publicly-accessible
+[code search](https://cs.android.com/androidx/platform/frameworks/support) that
+allows you to explore all of the source code in the repository. Links to this
+URL may be shared on public Buganizer and other external sites.
+
+We recommend setting up a custom search engine in Chrome as a faster (and
+publicly-accessible) alternative to `cs/`.
+
+### Custom search engine for `androidx-master-dev` {#custom-search-engine}
+
+1. Open `chrome://settings/searchEngines`
+1. Click the `Add` button
+1. Enter a name for your search engine, ex. "AndroidX Code Search"
+1. Enter a keyword, ex. "csa"
+1. Enter the following URL:
+ `https://cs.android.com/search?q=%s&ss=androidx%2Fplatform%2Fframeworks%2Fsupport`
+1. Click the `Add` button
+
+Now you can select the Chrome omnibox, type in `csa` and press tab, then enter a
+query to search for, e.g. `AppCompatButton file:appcompat`, and press the
+`Enter` key to get to the search result page.
+
+## Develop in Android Studio {#studio}
+
+Library development uses a curated version of Android Studio to ensure
+compatibility between various components of the development workflow.
+
+From the `frameworks/support` directory, you can use `ANDROIDX_PROJECTS=MAIN
+./gradlew studio` to automatically download and run the correct version of
+Studio to work on main set of androidx projects. `ANDROIDX_PROJECTS` has several
+other options like `ANDROIDX_PROJECTS=ALL` to open other subsets of the
+projects.
+[settings.gradle](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:settings.gradle)
+file in the repository has these options listed.
+
+```shell
+ANDROIDX_PROJECTS=MAIN ./gradlew studio
+```
+
+Next, open the `framework/support` project root from your checkout. If Studio
+asks you which SDK you would like to use, select `Use project SDK`. Importing
+projects may take a while, but once that finishes you can use Studio as you
+normally would for application or library development -- right-click on a test
+or sample to run or debug it, search through classes, and so on.
+
+If you see any errors (red underlines), click Gradle's elephant button in the
+toolbar ("Sync Project with Gradle Files") and they should resolve once the
+build completes.
+
+> NOTE: You should choose "Use project SDK" when prompted by Studio. If you
+> picked "Android Studio SDK" by mistake, don't panic! You can fix this by
+> opening `File > Project Structure > Platform Settings > SDKs` and manually
+> setting the Android SDK home path to
+> `<project-root>/prebuilts/fullsdk-<platform>`.
+
+> NOTE: If Android Studio's UI looks scaled up, ex. twice the size it should be,
+> you may need to add the following line to your `studio64.vmoptions` file using
+> `Help -> Edit Custom VM Options`:
+>
+> ```
+> -Dsun.java2d.uiScale.enabled=false
+> ```
+
+## Making changes {#changes}
+
+Similar to Android framework development, library developmnent should occur in
+CL-specific working branches. Use `repo` to create, upload, and abandon local
+branches. Use `git` to manage changes within a local branch.
+
+```shell
+cd path/to/checkout/frameworks/support/
+repo start my_branch_name .
+# make necessary code changes
+# use git to commit changes
+repo upload --cbr -t .
+```
+
+The `--cbr` switch automatically picks the current repo branch for upload. The
+`-t` switch sets the Gerrit topic to the branch name, e.g. `my-branch-name`.
+
+## Building {#building}
+
+### Modules and Maven artifacts {#modules-and-maven-artifacts}
+
+To build a specific module, use the module's `assemble` Gradle task. For
+example, if you are working on `core` module use:
+
+```shell
+./gradlew core:core:assemble
+```
+
+Use the `-Pandroidx.allWarningsAsErrors` to make warnings fail your build (same
+as presubmits):
+
+```shell
+./gradlew core:core:assemble -Pandroidx.allWarningsAsErrors
+```
+
+To build every module, run the Lint verifier, verify the public API surface, and
+generate the local Maven repository artifact, use the `createArchive` Gradle
+task:
+
+```shell
+./gradlew createArchive
+```
+
+To run the complete build task that our build servers use, use the
+`buildOnServer` Gradle task:
+
+```shell
+./gradlew buildOnServer
+```
+
+### Attaching a debugger to the build
+
+Gradle tasks, including building a module, may be run or debugged from Android
+Studio's `Gradle` pane by finding the task to be debugged -- for example,
+`androidx > androidx > appcompat > appcompat > build > assemble` --
+right-clicking on it, and then selecting `Debug...`.
+
+Note that debugging will not be available until Gradle sync has completed.
+
+## From the command line
+
+Tasks may also be debugged from the command line, which may be useful if
+`./gradlew studio` cannot run due to a Gradle task configuration issue.
+
+1. From the configurations dropdown in Studio, select "Edit Configurations".
+1. Click the plus in the top left to create a new "Remote" configuration. Give
+ it a name and hit "Ok".
+1. Set breakpoints.
+1. Run your task with added flags: `./gradlew <your_task_here>
+ -Dorg.gradle.debug=true --no-daemon`
+1. Hit the "Debug" button to the right of the configuration dropdown to attach
+ to the process.
+
+#### Troubleshooting the debugger
+
+If you get a "Connection refused" error, it's likely because a gradle daemon is
+still running on the port specified in the config, and you can fix this by
+killing the running gradle daemons:
+
+```shell
+./gradlew --stop
+```
+
+Note: This is described in more detail in this
+[Medium article](https://medium.com/grandcentrix/how-to-debug-gradle-plugins-with-intellij-eef2ef681a7b).
+
+#### Attaching to an annotation processor
+
+Annotation processors run as part of the build, to debug them is similar to
+debugging the build.
+
+For a Java project:
+
+```shell
+./gradlew <your_project>:compileDebugJava --no-daemon --rerun-tasks -Dorg.gradle.debug=true
+```
+
+For a Kotlin project:
+
+```shell
+./gradlew <your_project>:compileDebugKotlin --no-daemon --rerun-tasks -Dorg.gradle.debug=true -Dkotlin.compiler.execution.strategy="in-process" -Dkotlin.daemon.jvm.options="-Xdebug,-Xrunjdwp:transport=dt_socket\,address=5005\,server=y\,suspend=n"
+```
+
+### Optional: Enabling internal menu in IntelliJ/Studio
+
+To enable tools such as `PSI tree` inside of IntelliJ/Studio to help debug
+Android Lint checks and Metalava, you can enable the
+[internal menu](https://www.jetbrains.org/intellij/sdk/docs/reference_guide/internal_actions/enabling_internal.html)
+which is typically used for plugin and IDE development.
+
+### Reference documentation {#docs}
+
+Our reference docs (Javadocs and KotlinDocs) are published to
+https://developer.android.com/reference/androidx/packages and may be built
+locally.
+
+NOTE `./gradlew tasks` always has the canonical task information! When in doubt,
+run `./gradlew tasks`
+
+#### Javadocs
+
+To build API reference docs for tip-of-tree Java source code, run the Gradle
+task:
+
+```
+./gradlew disttipOfTreeDocs
+```
+
+This will output docs in the zip file:
+`{androidx-master-dev}/out/dist/android-support-tipOfTree-docs-0.zip`, as well
+as in local html files that you can check from your browser:
+`{androidx-master-dev}/out/androidx/build/javadoc/tipOfTree/offline/reference/packages.html`
+
+#### KotlinDocs
+
+To build API reference docs for tip-of-tree Kotlin source code, run the Gradle
+task:
+
+```
+./gradlew distTipOfTreeDokkaDocs
+```
+
+This will output docs in the zip file:
+`{androidx-master-dev}/out/dist/dokkaTipOfTreeDocs-0.zip`
+
+#### Release docs
+
+To build API reference docs for published artifacts formatted for use on
+[d.android.com](http://d.android.com), run the Gradle command:
+
+```
+./gradlew distpublicDocs
+```
+
+This will create the artifact
+`{androidx-master-dev}/out/dist/android-support-public-docs-0.zip`. This command
+builds docs based on the version specified in
+`{androidx-master-dev-checkout}/frameworks/support/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt`
+and uses the prebuilt checked into
+`{androidx-master-dev-checkout}/prebuilts/androidx/internal/androidx/`. We
+colloquially refer to this two step process of (1) updating PublishDocsRules.kt
+and (2) checking in a prebuilt artifact into the prebuilts directory as
+[The Prebuilts Dance](releasing.md#the-prebuilts-dance™). So, to build javadocs
+that will be published to
+https://developer.android.com/reference/androidx/packages, both of these steps
+need to be completed.
+
+Once you done the above steps, Kotlin docs will also be generated, with the only
+difference being that we use the Gradle command:
+
+```
+./gradlew distPublicDokkaDocs
+```
+
+which generates the kotlin docs artifact
+`{androidx-master-dev}/out/dist/dokkaPublicDocs-0.zip`
+
+### Updating public APIs {#updating-public-apis}
+
+Public API tasks -- including tracking, linting, and verifying compatibility --
+are run under the following conditions based on the `androidx` configuration
+block, evaluated in order:
+
+* `runApiTasks=Yes` => yes
+* `runApiTasks=No` => no
+* `toolingProject=true` => no
+* `mavenVersion` or group version not set => no
+* Has an existing `api/` directory => yes
+* `publish=SNAPSHOT_AND_RELEASE` => yes
+* Otherwise, no
+
+If you make changes to tracked public APIs, you will need to acknowledge the
+changes by updating the `<component>/api/current.txt` and associated API files.
+This is handled automatically by the `updateApi` Gradle task:
+
+```shell
+# Run updateApi for all modules.
+./gradlew updateApi
+
+# Run updateApi for a single module, ex. appcompat-resources in group appcompat.
+./gradlew :appcompat:appcompat-resources:updateApi
+```
+
+If you change the public APIs without updating the API file, your module will
+still build **but** your CL will fail Treehugger presubmit checks.
+
+### Release notes & the `Relnote:` tag {#relnote}
+
+Prior to releasing, release notes are pre-populated using a script and placed
+into a Google Doc. The Google Doc is manually double checked by library owners
+before the release goes live. To auto-populate your release notes, you can use
+the semi-optional commit tag `Relnote:` in your commit, which will automatically
+include that message the commit in the pre-populated release notes.
+
+The presence of a `Relnote:` tag is required for API changes in
+`androidx-master-dev`.
+
+#### How to use it?
+
+One-line release note:
+
+``` {.good}
+Relnote: Fixed a critical bug
+```
+
+``` {.good}
+Relnote: "Fixed a critical bug"
+```
+
+``` {.good}
+Relnote: Added the following string function: `myFoo(\"bar\")`
+```
+
+Multi-line release note:
+
+Note: If the following lines do not contain an indent, you may hit b/165570183.
+
+``` {.good}
+Relnote: "We're launching this awesome new feature! It solves a whole list of
+ problems that require a lot of explaining! "
+```
+
+``` {.good}
+Relnote: """Added the following string function: `myFoo("bar")`
+ It will fix cases where you have to call `myFoo("baz").myBar("bar")`
+ """
+```
+
+Opt out of the Relnote tag:
+
+``` {.good}
+Relnote: N/A
+```
+
+``` {.good}
+Relnote: NA
+```
+
+NOT VALID:
+
+``` {.bad}
+Relnote: This is an INVALID multi-line release note. Our current scripts won't
+include anything beyond the first line. The script has no way of knowing when
+the release note actually stops.
+```
+
+``` {.bad}
+Relnote: This is an INVALID multi-line release note. "Quotes" need to be
+ escaped in order for them to be parsed properly.
+```
+
+### Common build errors
+
+#### Diagnosing build failures
+
+If you've encountered a build failure and you're not sure what is triggering it,
+then please run
+`./development/diagnose-build-failure/diagnose-build-failure.sh`.
+
+This script can categorize your build failure into one of the following
+categories:
+
+* The Gradle Daemon is saving state in memory and triggering a failure
+* Your source files have been changed and/or incompatible git commits have
+ been checked out
+* Some file in the out/ dir is triggering an error
+ * If this happens, diagnose-build-failure.sh should also identify which
+ file(s) specifically
+* The build is nondeterministic and/or affected by timestamps
+* The build via gradlew actually passes and this build failure is specific to
+ Android Studio
+
+Some more-specific build failures are listed below in this page.
+
+#### Out-of-date platform prebuilts
+
+Like a normal Android library developed in Android Studio, libraries within
+`androidx` are built against prebuilts of the platform SDK. These are checked in
+to the `prebuilts/fullsdk-darwin/platforms/<android-version>` directory.
+
+If you are developing against pre-release platform APIs in the internal
+`androidx-platform-dev` branch, you may need to update these prebuilts to obtain
+the latest API changes.
+
+### Missing external dependency
+
+If Gradle cannot resolve a dependency listed in your `build.gradle`, you may
+need to import the corresponding artifact into `prebuilts/androidx/external`.
+Our workflow does not automatically download artifacts from the internet to
+facilitate reproducible builds even if remote artifacts are changed.
+
+You can download a dependency by running:
+
+```shell
+cd frameworks/support && ./development/importMaven/import_maven_artifacts.py -n 'someGroupId:someArtifactId:someVersion'
+```
+
+This will create a change within the `prebuilts/androidx/external` directory.
+Make sure to upload this change before or concurrently (ex. in the same Gerrit
+topic) with the dependent library code.
+
+Libraries typically reference dependencies using constants defined in
+[`Dependencies.kt`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt),
+so please update this file to include a constant for the version of the library
+that you have checked in. You will reference this constant in your library's
+`build.gradle` dependencies.
+
+#### Updating an existing dependency
+
+If an older version of a dependency prebuilt was already checked in, please
+manually remove it within the same CL that adds the new prebuilt. You will also
+need to update `Dependencies.kt` to reflect the version change.
+
+#### My gradle build fails with "Cannot invoke method getURLs() on null object"
+
+You're using Java 9's javac, possibly because you ran envsetup.sh from the
+platform build or specified Java 9 as the global default Java compiler. For the
+former, you can simply open a new shell and avoid running envsetup.sh. For the
+latter, we recommend you set Java 8 as the default compiler using sudo
+update-java-alternatives; however, if you must use Java 9 as the default then
+you may alternatively set JAVA_HOME to the location of the Java 8 SDK.
+
+#### My gradle build fails with "error: cannot find symbol" after making framework-dependent changes.
+
+You probably need to update the prebuilt SDK used by the gradle build. If you
+are referencing new framework APIs, you will need to wait for the framework
+changes to land in an SDK build (or build it yourself) and then land in both
+prebuilts/fullsdk and prebuilts/sdk. See
+[Updating SDK prebuilts](playbook.md#prebuilts-fullsdk) for more information.
+
+#### How do I handle refactoring a framework API referenced from a library?
+
+Because AndroidX must compile against both the current framework and the latest
+SDK prebuilt, and because compiling the SDK prebuilt depends on AndroidX, you
+will need to refactor in stages: Remove references to the target APIs from
+AndroidX Perform the refactoring in the framework Update the framework prebuilt
+SDK to incorporate changes in (2) Add references to the refactored APIs in
+AndroidX Update AndroidX prebuilts to incorporate changes in (4)
+
+## Testing {#testing}
+
+AndroidX libraries are expected to include unit or integration test coverage for
+100% of their public API surface. Additionally, all CLs must include a `Test:`
+stanza indicating which tests were used to verify correctness. Any CLs
+implementing bug fixes are expected to include new regression tests specific to
+the issue being fixed
+
+See the [Testing](testing.md) page for more resources on writing, running, and
+monitoring tests.
+
+### AVD Manager
+
+The Android Studio instance started by `./gradlew studio` uses a custom SDK
+directory, which means any virtual devices created by a "standard" non-AndroidX
+instance of Android Studio will be _visible_ from the `./gradlew studio`
+instance but will be unable to locate the SDK artifacts -- they will display a
+`Download` button.
+
+You can either use the `Download` button to download an extra copy of the SDK
+artifacts _or_ you can set up a symlink to your "standard" non-AndroidX SDK
+directory to expose your existing artifacts to the `./gradlew studio` instance:
+
+```shell
+# Using the default MacOS Android SDK directory...
+ln -s /Users/$(whoami)/Library/Android/sdk/system-images \
+ ../../prebuilts/fullsdk-darwin/system-images
+```
+
+### Benchmarking {#testing-benchmarking}
+
+Libraries are encouraged to write and monitor performance benchmarks. See the
+[Benchmarking](benchmarking.md) page for more details.
+
+## Library snapshots {#snapshots}
+
+### Quick how to
+
+Add the following snippet to your build.gradle file, replacing `buildId` with a
+snapshot build Id.
+
+```groovy {highlight=context:[buildId]}
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ maven { url 'https://androidx.dev/snapshots/builds/[buildId]/artifacts/repository' }
+ }
+}
+```
+
+You must define dependencies on artifacts using the SNAPSHOT version suffix, for
+example:
+
+```groovy {highlight=context:SNAPSHOT}
+dependencies {
+ implementation "androidx.core:core:1.2.0-SNAPSHOT"
+}
+```
+
+### Where to find snapshots
+
+If you want to use unreleased `SNAPSHOT` versions of `androidx` artifacts, you
+can find them on either our public-facing build server:
+
+`https://ci.android.com/builds/submitted/<build_id>/androidx_snapshot/latest`
+
+or on our slightly-more-convenient [androidx.dev](https://androidx.dev) site:
+
+`https://androidx.dev/snapshots/builds/<build-id>/artifacts/repository` for a
+specific build ID
+
+`https://androidx.dev/snapshots/builds/latest/artifacts/repository` for
+tip-of-tree snapshots
+
+### Obtaining a build ID
+
+To browse build IDs, you can visit either
+[androidx-master-dev](https://ci.android.com/builds/branches/aosp-androidx-master-dev/grid?)
+on ci.android.com or [Snapshots](https://androidx.dev/snapshots/builds) on the
+androidx.dev site.
+
+Note that if you are using androidx.dev, you may substitute `latest` for a build
+ID to use the last known good build.
+
+To manually find the last known good `build-id`, you have several options.
+
+#### Snapshots on androidx.dev
+
+[Snapshots](https://androidx.dev/snapshots/builds) on androidx.dev only lists
+usable builds.
+
+#### Programmatically via `jq`
+
+Install `jq`:
+
+```shell
+sudo apt-get install jq
+```
+
+```shell
+ID=`curl -s "https://ci.android.com/builds/branches/aosp-androidx-master-dev/status.json" | jq ".targets[] | select(.ID==\"aosp-androidx-master-dev.androidx_snapshot\") | .last_known_good_build"` \
+ && echo https://ci.android.com/builds/submitted/"${ID:1:-1}"/androidx_snapshot/latest/raw/repository/
+```
+
+#### Android build server
+
+Go to
+[androidx-master-dev](https://ci.android.com/builds/branches/aosp-androidx-master-dev/grid?)
+on ci.android.com.
+
+For `androidx-snapshot` target, wait for the green "last known good build"
+button to load and then click it to follow it to the build artifact URL.
+
+### Using in a Gradle build
+
+To make these artifacts visible to Gradle, you need to add it as a respository:
+
+```groovy
+allprojects {
+ repositories {
+ google()
+ maven {
+ // For all Jetpack libraries (including Compose)
+ url 'https://androidx.dev/snapshots/builds/<build-id>/artifacts/repository'
+ }
+ }
+}
+```
+
+Note that the above requires you to know the `build-id` of the snapshots you
+want.
+
+#### Specifying dependencies
+
+All artifacts in the snapshot repository are versioned as `x.y.z-SNAPSHOT`. So
+to use a snapshot artifact, the version in your `build.gradle` will need to be
+updated to `androidx.<groupId>:<artifactId>:X.Y.Z-SNAPSHOT`
+
+For example, to use the `core:core:1.2.0-SHAPSHOT` snapshot, you would add the
+following to your `build.gradle`:
+
+```
+dependencies {
+ ...
+ implementation("androidx.core:core:1.2.0-SNAPSHOT")
+ ...
+}
+```
+
+## FAQ {#faq}
+
+### How do I test my change in a separate Android Studio project? {#faq-test-change-studio}
+
+If you're working on a new feature or bug fix in AndroidX, you may want to test
+your changes against another project to verify that the change makes sense in a
+real-world context or that a bug's specific repro case has been fixed.
+
+If you need to be absolutely sure that your test will exactly emulate the
+developer's experience, you can repeatedly build the AndroidX archive and
+rebuild your application. In this case, you will need to create a local build of
+AndroidX's local Maven repository artifact and install it in your Android SDK
+path.
+
+First, use the `createArchive` Gradle task to generate the local Maven
+repository artifact:
+
+```shell
+# Creates <path-to-checkout>/out/dist/sdk-repo-linux-m2repository-##.zip
+./gradlew createArchive
+```
+
+Next, take the ZIP output from this task and extract the contents to the Android
+SDK path that you are using for your alternate (non-AndroidX) version of Android
+Studio. For example, you may be using `~/Android/SDK/extras` if you are using
+the default Android Studio SDK for app development or
+`prebuilts/fullsdk-linux/extras` if you are using fullsdk for platform
+development.
+
+```shell
+# Creates or overwrites android/m2repository
+cd <path-to-sdk>/extras
+unzip <path-to-checkout>/out/dist/top-of-tree-m2repository-##.zip
+```
+
+In the project's 'build.gradle' within 'repositories' notify studio of the
+location of m2repository:
+
+```groovy
+allprojects {
+ repositories {
+ ...
+ maven {
+ url "<path-to-sdk>/extras/m2repository"
+ }
+ }
+}
+```
+
+NOTE Gradle resolves dependencies in the order that the repositories are defined
+(if 2 repositories can resolve the same dependency, the first listed will do so
+and the second will not). Therefore, if the library you are testing has the same
+group, artifact, and version as one already published, you will want to list
+your custom maven repo first.
+
+Finally, in the dependencies section of your standalone project's `build.gradle`
+file, add or update the `implementation` entries to reflect the AndroidX modules
+that you would like to test. Example:
+
+```
+dependencies {
+ ...
+ implementation "androidx.appcompat:appcompat::1.0.0-alpha02"
+}
+```
+
+If you are testing your changes in the Android Platform code, you can replace
+the module you are testing
+`YOUR_ANDROID_PATH/prebuilts/sdk/current/androidx/m2repository` with your own
+module. We recommend only replacing the module you are modifying instead of the
+full m2repository to avoid version issues of other modules. You can either take
+the unzipped directory from
+`<path-to-checkout>/out/dist/top-of-tree-m2repository-##.zip`, or from
+`<path-to-checkout>/out/androidx/build/support_repo/` after buiding `androidx`.
+Here is an example of replacing the RecyclerView module:
+
+```shell
+$TARGET=YOUR_ANDROID_PATH/prebuilts/sdk/current/androidx/m2repository/androidx/recyclerview/recyclerview/1.1.0-alpha07;
+rm -rf $TARGET;
+cp -a <path-to-sdk>/extras/m2repository/androidx/recyclerview/recyclerview/1.1.0-alpha07 $TARGET
+```
+
+Make sure the library versions are the same before and after replacement. Then
+you can build the Android platform code with the new `androidx` code.
diff --git a/docs/onboarding_images/image1.png b/docs/onboarding_images/image1.png
new file mode 100644
index 0000000..9a32d42
--- /dev/null
+++ b/docs/onboarding_images/image1.png
Binary files differ
diff --git a/docs/onboarding_images/image2.png b/docs/onboarding_images/image2.png
new file mode 100644
index 0000000..9c215f1
--- /dev/null
+++ b/docs/onboarding_images/image2.png
Binary files differ
diff --git a/docs/onboarding_images/image3.png b/docs/onboarding_images/image3.png
new file mode 100644
index 0000000..e672255
--- /dev/null
+++ b/docs/onboarding_images/image3.png
Binary files differ
diff --git a/docs/policies.md b/docs/policies.md
new file mode 100644
index 0000000..b099299
--- /dev/null
+++ b/docs/policies.md
@@ -0,0 +1,190 @@
+## AndroidX policies and processes
+
+This document is intended to describe release policies that affect the workflow
+of an engineer developing within the AndroidX libraries. It also describes the
+process followed by a release engineer or TPM to take a development branch and
+publish it as a release on Google Maven.
+
+Policies and processes automated via tooling are noted in
+<span style="color:#bf9000;">yellow</span>.
+
+[TOC]
+
+## Project directory structure {#directory-structure}
+
+Libraries developed in AndroidX follow a consistent project naming and directory
+structure.
+
+Library groups should organize their modules into directories and module names
+(in brackets) as:
+
+```
+<feature-name>/
+ <feature-name>-<sub-feature>/ [<feature-name>:<feature-name>-<sub-feature>]
+ integration-tests/
+ testapp/ [<feature-name>:testapp]
+ testlib/ [<feature-name>:testlib]
+ samples/ [<feature-name>:samples]
+```
+
+For example, the `room` library group's directory structure is:
+
+```
+room/
+ common/ [room:room-common]
+ ...
+ rxjava2/ [room:room-rxjava2]
+ testing/ [room:room-testing]
+ integration-tests/
+ testapp/ [room:testapp]
+ testapp-kotlin/ [room:testapp-kotlin]
+```
+
+## Terminology {#terminology}
+
+**Artifact**
+: Previously referred to as "a Support Library library." A library --
+ typically Java or Android -- that maps to a single Maven artifact, ex.
+ `androidx.recyclerview:recyclerview`. An artifact is associated with a
+ single Android Studio module and a directory containing a `build.gradle`
+ configuration, resources, and source code.
+
+**API Council**
+: A committee that reviews Android APIs, both platform and library, to ensure
+ they are consistent and follow the best-practices defined in our API
+ guidelines.
+
+**Semantic Versioning (SemVer)**
+: A versioning standard developed by one of the co-founders of GitHub that is
+ understood by common dependency management systems, including Maven. In this
+ document, we are referring specifically to
+ [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html).
+
+## Managing versions {#managing-versions}
+
+This section outlines the steps for a variety of common versioning-related
+tasks. Artifact versions should **only** be modified by their owners as
+specified in the artifact directory's `OWNERS` file.
+
+Artifact versions are specified in
+[`LibraryVersions.kt`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt).
+Versions are bound to your artifact in the `supportLibrary` block in your
+artifact's `build.gradle` file. The `Version` class validates the version string
+at build time.
+
+In the
+[`LibraryVersions.kt`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt)
+file:
+
+```
+object LibraryVersions {
+ val SNAZZY_ARTIFACT = Version("1.1.0-alpha03")
+}
+```
+
+In your artifact's `build.gradle` file:
+
+```
+import androidx.build.LibraryVersions
+
+supportLibrary {
+ mavenVersion = LibraryVersions.SNAZZY_ARTIFACT
+}
+```
+
+#### Finalizing for release {#finalizing-for-release}
+
+When the artifact is ready for release, its versioned API file should be
+finalized to ensure that the subsequent versioned release conforms to the
+versioning policies.
+
+```
+./gradlew <module>:finalizeApi
+```
+
+This will prevent any subsequent changes to the API surface until the artifact
+version is updated. To update the artifact version and allow changes within the
+semantic versioning contract, simply update the version string in the artifact's
+`build.gradle` (see [Workflow](#workflow) introduction).
+
+To avoid breaking the development workflow, we recommend that API finalization
+and version string updates be submitted as a single CL.
+
+## Dependencies {#dependencies}
+
+Artifacts may depend on other artifacts within AndroidX as well as sanctioned
+third-party libraries.
+
+### Versioned artifacts {#versioned-artifacts}
+
+One of the most difficult aspects of independently-versioned releases is
+maintaining compatibility with public artifacts. In a mono repo such as Google's
+repository or Android Git at master revision, it's easy for an artifact to
+accidentally gain a dependency on a feature that may not be released on the same
+schedule.
+
+#### Pre-release dependencies {#pre-release-dependencies}
+
+Pre-release suffixes **must** propagate up the dependency tree. For example, if
+your artifact has API-type dependencies on pre-release artifacts, ex.
+`1.1.0-alpha01`, then your artifact must also carry the `alpha` suffix. If you
+only have implementation-type dependencies, your artifact may carry either the
+`alpha` or `beta` suffix.
+
+#### Pinned versions {#pinned-versions}
+
+To avoid issues with dependency versioning, consider pinning your artifact's
+dependencies to the oldest version (available via local `maven_repo` or Google
+Maven) that satisfies the artifact's API requirements. This will ensure that the
+artifact's release schedule is not accidentally tied to that of another artifact
+and will allow developers to use older libraries if desired.
+
+```
+dependencies {
+ api("androidx.collection:collection:1.0.0")
+ ...
+}
+```
+
+Artifacts should be built and tested against both pinned and tip-of-tree
+versions of their dependencies to ensure behavioral compatibility.
+
+#### Non-Pinned versions {#nonpinned-versions}
+
+Below is an example of a non-pinned dependency. It ties the artifact's release
+schedule to that of the dependency artifact, because the dependency will need to
+be released at the same time.
+
+```
+dependencies {
+ api(project(":collection"))
+ ...
+}
+```
+
+### Non-public APIs {#non-public-apis}
+
+Artifacts may depend on non-public (e.g. `@hide`) APIs exposed within their own
+artifact or another artifact in the same `groupId`; however, cross-artifact
+usages are subject to binary compatibility guarantees and
+`@RestrictTo(Scope.LIBRARY_GROUP)` APIs must be tracked like public APIs.
+
+```
+Dependency versioning policies are enforced at build time in the createArchive task. This task will ensure that pre-release version suffixes are propagated appropriately.
+
+Cross-artifact API usage policies are enforced by the checkApi and checkApiRelease tasks (see Life of a release).
+```
+
+### Third-party libraries {#third-party-libraries}
+
+Artifacts may depend on libraries developed outside of AndroidX; however, they
+must conform to the following guidelines:
+
+* Prebuilt **must** be checked into Android Git with both Maven and Make
+ artifacts
+ * `prebuilts/maven_repo` is recommended if this dependency is only
+ intended for use with AndroidX artifacts, otherwise please use
+ `external`
+* Prebuilt directory **must** contains an OWNERS file identifying one or more
+ individual owners (e.g. NOT a group alias)
+* Library **must** be approved by legal
diff --git a/docs/principles.md b/docs/principles.md
new file mode 100644
index 0000000..6dba721
--- /dev/null
+++ b/docs/principles.md
@@ -0,0 +1,135 @@
+# Jetpack Principles
+
+[TOC]
+
+## Ethos of Jetpack
+
+To create components, tools, and guidance that makes it quick and easy to build
+great Android apps, including contributions from Google and the open-source
+community.
+
+## Core Principles of a Jetpack Library
+
+Jetpack libraries provide the following guarantees to Android Developers:
+
+_formatted as “Jetpack libraries are…” with sub-points “Libraries should…”_
+
+### 1. Optimized for external client adoption
+
+- Libraries should work for first-party clients and may even have optional
+ modules tailored specifically to first-party needs, but primary
+ functionality should target external developers.
+- Measure success by 3p client adoption, followed by 1p client adoption.
+
+### 2. Designed to satisfy real-world use cases
+
+- Meet developers where they are and solve the problems that they have
+ building apps -- not designed to just provide parity with existing platform
+ APIs and features
+- Expose modules that are tightly-scoped to **developer pain points**
+ - Smaller building blocks for external developers by scoping disjoint use
+ cases that are likely not to co-exist in a single app to individual
+ modules.
+- Implement layered complexity, with **simple top-level APIs**
+ - Complicated use case support must not be at the expense of increasing
+ API complexity for the most common simpler use cases.
+- Have **backing data or a researched hypothesis** (research, demand etc) to
+ prove the library is necessary and sufficient.
+
+### 3. Aware of the existing developer ecosystem
+
+- Avoid reinventing the wheel -- do not create a new library where one already
+ exists that is accepted by the community as a best practice
+
+### 4. Consistent with the rest of Jetpack
+
+- Ensure that concepts learned in one component can be seen and understood in
+ other components
+- Leverage Jetpack and community standards, for example:
+ - For async work, uses Kotlin coroutines and/or Kotlin flow
+ - For data persistence, uses Jetpack DataStore for simple and small data
+ and uses Room for more complicated Data
+
+### 5. Developed as open-source and compatible with AOSP Android
+
+- Expose a unified developer-facing API surface across the Android ecosystem
+- Avoid proprietary services or closed-source libraries for core
+ functionality, and instead provide integration points that allow a developer
+ to choose a proprietary service as the backing implementation
+- Develop in AOSP to provide visibility into new features and bug fixes and
+ encourage external participation
+
+### 6. Written using language-idiomatic APIs
+
+- Write APIs that feel natural for clients using both
+ [Kotlin](https://developer.android.com/kotlin/interop) and Java
+
+### 7. Compatible with a broad range of API levels
+
+- Support older platforms and API levels according to client needs
+- Provide continued maintenance to ensure compatibility with newer platforms
+- Design with the expectation that every Jetpack API is **write-once,
+ run-everywhere** for Android with graceful degradation where necessary
+
+### 8. Integrated with best practices
+
+- Guide developers toward using existing Jetpack best-practice libraries,
+ including Architecture Components
+
+### 9. Designed for tooling and testability
+
+- Write adequate unit and integration tests for the library itself
+- Provide developers with an accompanying testing story for integration
+ testing their own applications (ex. -testing artifacts that some libraries
+ expose)
+ - Robolectric shouldn’t need to shadow the library classes
+ - Ex. Room has in-memory testing support
+- Build tooling concurrent with the library when possible, and with tooling in
+ mind otherwise
+
+### 10. Released using a clearly-defined process
+
+- Follow Semantic Versioning and pre-release revision guidelines where each
+ library moves through alpha, beta, and rc revisions to gain feedback and
+ ensure stability
+
+### 11. Well-documented
+
+- Provide developers with getting started and use case documentation on
+ d.android.com in addition to clear API reference documentation
+
+### 12. Supported for long-term use
+
+- Plan for long-term support and maintenance
+
+### 13. Examples of modern development
+
+- Where possible, targeting the latest languages, OS features, and tools. All
+ new libraries should be written in Kotlin first. Existing libraries
+ implemented in Java should add Kotlin extension libraries to improve the
+ interoperability of the Java APIs from Kotlin. New libraries written in Java
+ require a significant business reason on why a dependency in Kotlin cannot
+ be taken. The following is the order of preference, with each lower tier
+ requiring a business reason:
+ 1. Implemented in Kotlin that compiles to Java 8 bytecode
+ 2. Implemented in Java 8, with `-ktx` extensions for Kotlin
+ interoperability
+ 3. Implemented in Java 7, with `-ktx` extensions for Kotlin
+ interoperability
+
+### 14. High quality APIs and ownership
+
+- All Jetpack libraries are expected to abide by Android and Jetpack API
+ Guidelines
+
+## Target Audience
+
+Jetpack libraries are used by a wide variety of external developers, from
+individuals working on their first Android app to huge corporations developing
+large-scale production apps. Generally, however, Jetpack libraries are designed
+to focus on small- to medium-sized app development teams.
+
+- Note: If the library targets a niche set of apps, the developer pain
+ point(s) addressed by the Jetpack library must be significant enough to
+ justify its need.
+ - Example : Jetpack Enterprise library
diff --git a/docs/testing.md b/docs/testing.md
new file mode 100644
index 0000000..b937bb9
--- /dev/null
+++ b/docs/testing.md
@@ -0,0 +1,246 @@
+# Testing
+
+[TOC]
+
+AndroidX contains unit and integration tests that are run automatically when a
+change is uploaded. It also contains a number of sample applications that are
+useful for demonstrating how to use features as well as performing manual
+testing.
+
+## Adding tests {#adding}
+
+For an example of how to set up simple unit and integration tests in a new
+module, see
+[aosp/1189799](https://android-review.googlesource.com/c/platform/frameworks/support/+/1189799).
+For an example of how to set up Espresso-powered integration tests, see the
+`preference` library's
+[`build.gradle`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:preference/preference/build.gradle)
+and
+[`EditTextPreferenceTest.java`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:preference/preference/src/androidTest/java/androidx/preference/tests/EditTextPreferenceTest.java)
+files.
+
+The currently allowed test runners for on-device tests are
+[`AndroidJUnitRunner`](https://developer.android.com/training/testing/junit-runner)
+and
+[`Parameterized`](https://junit.org/junit4/javadoc/4.12/org/junit/runners/Parameterized.html).
+
+### What gets tested, and when
+
+We use the
+[AffectedModuleDetector](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:buildSrc/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt)
+to determine what projects have changed since the last merge.
+
+In presubmit, "affected" modules will run all host and device tests regardless
+of size. Modules that _depend_ on affected modules will run all host tests, but
+will only run device tests annotated with `@SmallTest` or `@MediumTest`.
+
+When changes are made that can't be associated with a module, are in the root of
+the checkout, or are within `buildSrc`, then all host tests and all device tests
+annotated with `@SmallTest` or `@MediumTest` will be run for all modules.
+
+Presubmit tests represent only a subset of the devices on which our tests run.
+The remaining devices are tested only in postsubmit. In postsubmit, all host and
+device tests are run for all modules.
+
+### Test annotations
+
+#### Test size
+
+All device tests *should* be given a size annotation, which is one of:
+
+* [`@SmallTest`](https://developer.android.com/reference/androidx/test/filters/SmallTest)
+* [`@MediumTest`](https://developer.android.com/reference/androidx/test/filters/MediumTest)
+* [`@LargeTest`](https://developer.android.com/reference/androidx/test/filters/LargeTest)
+
+If a device test is _not_ annotated with its size, it will be considered a
+`@LargeTest` by default. Host tests do not need to be annotated with their size,
+as all host tests are run regardless of size.
+
+This annotation can occur at either the class level or individual test level.
+After API level 27, timeouts are enforced based on this annotation.
+
+Annotation | Max duration | Timeout after
+------------- | ------------ | -------------
+`@SmallTest` | 200ms | 300ms
+`@MediumTest` | 1000ms | 1500ms
+`@LargeTest` | 100000ms | 120000ms
+
+Small tests should be less than 200ms, and the timeout is set to 300ms. Medium
+tests should be less than 1000ms, and the timeout is set to 1500ms. Large tests
+have a timeout of 120000ms, which should cover any remaining tests.
+
+The exception to this rule is when using a runner other than
+[`AndroidJUnitRunner`](https://developer.android.com/training/testing/junit-runner).
+Since these runners do not enforce timeouts, tests that use them must not use a
+size annotation. They will run whenever large tests would run.
+
+Currently the only allowed alternative is the
+[`Parameterized`](https://junit.org/junit4/javadoc/4.12/org/junit/runners/Parameterized.html)
+runner. If you need to use a different runner for some reason, please reach out
+to the team and we can review/approve the use.
+
+#### Disabling tests
+
+To disable a device-side test in presubmit testing only -- but still have it run
+in postsubmit -- use the
+[`@FlakyTest`](https://developer.android.com/reference/androidx/test/filters/FlakyTest)
+annotation. There is currently no support for presubmit-only disabling of
+host-side tests.
+
+If you need to stop a host- or device-side test from running entirely, use
+JUnit's [`@Ignore`](http://junit.sourceforge.net/javadoc/org/junit/Ignore.html)
+annotation. Do *not* use Android's `@Suppress` annotation, which only works with
+Android test runners and will *not* work for host-side tests.
+
+#### Filtering devices
+
+To restrict a test to a range of SDKs, use
+[`@SdkSuppress`](https://developer.android.com/reference/androidx/test/filters/SdkSuppress)
+which allows specifying a range with `minSdkVersion` and `maxSdkVersion`. This
+annotation also supports targeting a specific pre-release SDK with the
+`codeName` parameter.
+
+```java
+// Target SDKs 17 through 19, inclusive
+@SdkSuppress(minSdkVersion = 17, maxSdkVersion = 19)
+
+// Target pre-release SDK R only
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R, isCodeName = "R")
+```
+
+You may also gate portions of test implementation code using `SDK_INT` or
+[`BuildCompat.isAtLeast`](https://developer.android.com/reference/androidx/core/os/BuildCompat)
+methods.
+
+To restrict to only phsyical devices, use
+[`@RequiresDevice`](https://developer.android.com/reference/androidx/test/filters/RequiresDevice).
+
+### Animations in tests
+
+Animations are disabled for tests by default. This helps avoid flakes due to
+timing and also makes tests faster.
+
+In rare cases, like testing the animations themselves, you may want to enable
+animations for a particular test or test class. For those cases, you can use the
+[`AnimationDurationScaleRule`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:testutils/testutils-runtime/src/main/java/androidx/testutils/AnimationDurationScaleRule.kt).
+
+## Using the emulator {#emulator}
+
+You can use the emulator or a real device to run tests. If you wish to use the
+emulator, you will need to access the AVD Manager (and your downloaded emulator
+images) using a separate "normal" instance of Android Studio. "Normal" means a
+non-Canary build of Studio that you would use for regular app development -- the
+important part being that it points to the Android SDK where your downloaded
+emulator images reside. You will need to open a project to get the Tools menu --
+do NOT open the AndroidX project in the "normal" instance of Android Studio;
+instead, open a normal app or create a blank project using the app wizard.
+
+## Debugging with platform SDK sources {#sources}
+
+The platform SDK sources that are checked into the development branch may not
+match up with the build of Android present on the emulator or your physical
+device. As a result, the line numbers reported by the debugger may not match up
+the actual code being run.
+
+If you have a copy of the sources for the build against which you are debugging,
+you can manually specify your platform SDK source path:
+
+1. Click on a module (e.g. `appcompat`) in the `Project` view
+1. Press `Ctrl-Shift-A` and type "Module Settings", then run the action
+1. In the `Project Structure` dialog, navigate to `SDKs > Android API 29
+ Platform > Sourcepath`
+1. Use the `-` button to remove any paths that are present, then use the `+`
+ button to add the desired source path, ex. `<android checkout
+ root>/frameworks/base` if you are debugging against a locally-built system
+ image
+
+NOTE The `Project Structure` dialog reachable via `File > Project Structure` is
+**not** the same as the `Project Structure` dialog that will allow you to
+specify the SDK source path. You must use the "Module Settings" action as
+directed above.
+
+## Running unit and integration tests {#running}
+
+From Android Studio, right-click can be used to run most test targets, including
+source files, classes within a file, or individual test methods but **not**
+entire modules. To run a supported test target, right-click on the test target
+and then click `Run <name of test target>`.
+
+To run tests for an entire module such as `appcompat`, use `Run -> Edit
+configurations...` and use the `+` button to create a new `Android Instrumented
+Tests` configuration. Specify the module to be tested, give it a reasonable name
+(not "All Tests") and click `OK`, then use the `Run` menu to run the
+configuration.
+
+![alt_text](onboarding_images/image2.png "screenshot of run menu")
+
+NOTE If you receive the error `JUnit version 3.8 or later expected` this means
+that Android Studio generated an Android JUnit configuration when you actually
+needed an Android Instrumented Tests configuration. Open the `Run -> Edit
+configurations...` dialog and delete the configuration from Android JUnit, then
+manually add a configuration in Android Instrumented Tests.
+
+### From the command line {#running-from-shell}
+
+Following a successful build, tests may be run against a particular AndroidX
+module using `gradlew`.
+
+To run all integration tests in a specific project, run the following from
+`framework/support`:
+
+```shell
+./gradlew <project-name>:connectedCheck --info --daemon
+```
+
+substituting the Gradle project name (ex. `core`).
+
+To run all integration tests in the specific project and test class you're
+working on, run
+
+```shell
+./gradlew <project-name>:connectedCheck --info --daemon \
+ -Pandroid.testInstrumentationRunnerArguments.class=<fully-qualified-class>[\#testName]
+```
+
+substituting the Gradle project name (ex. `viewpager`) and fully-qualified class
+name (ex. `androidx.viewpager.widget.ViewPagerTest`) of your test file,
+optionally followed by `\#testName` if you want to execute a single test in that
+file.
+
+If you see some weird compilation errors such as below, run `./gradlew clean`
+first:
+
+```
+Unknown source file : UNEXPECTED TOP-LEVEL EXCEPTION:
+Unknown source file : com.android.dex.DexException: Multiple dex files define Landroid/content/pm/ParceledListSlice$1;
+```
+
+## Test apps {#testapps}
+
+Library developers are strongly encouraged to write test apps that exercise
+their library's public API surface. Test apps serve multiple purposes:
+
+* Integration testing and validation of API testability, when paired with
+ tests
+* Validation of API usability and developer experience, when paired with a use
+ case or critical user journey
+* Sample documentation, when embedded into API reference docs using the
+ [`@sample` and `@Sampled` annotations](api_guidelines.md#sample-usage)
+
+### Legacy test apps {#testapps-legacy}
+
+We have a set of legacy sample Android applications in projects suffixed with
+`-demos`. These applications do not have tests and should not be used as test
+apps for new APIs, but they may be useful for manual regression testing.
+
+1. Click `Run/Debug Configuration` on the top of the window.
+1. Select the app you want to run.
+1. Click 'Run' button.
+
+![alt_text](onboarding_images/image3.png "screenshot of Run/Debug menu")
+
+## Benchmarking {#benchmarking}
+
+AndroidX supports benchmarking - locally with Studio/Gradle, and continuously in
+post-submit. For more information on how to create and run benchmarks, see
+[Benchmarking](benchmarking.md).
diff --git a/docs/truth_guide.md b/docs/truth_guide.md
new file mode 100644
index 0000000..fd9efc4
--- /dev/null
+++ b/docs/truth_guide.md
@@ -0,0 +1,147 @@
+# Adding custom Truth subjects
+
+[TOC]
+
+## Custom truth subjects
+
+Every subject class should extend
+[Subject](https://truth.dev/api/latest/com/google/common/truth/Subject.html) and
+follow the naming schema `[ClassUnderTest]Subject`. The Subject must also have a
+constructor that accepts
+[FailureMetadata](https://truth.dev/api/latest/com/google/common/truth/FailureMetadata.html)
+and a reference to the object under test, which are both passed to the
+superclass.
+
+```kotlin
+class NavControllerSubject private constructor(
+ metadata: FailureMetadata,
+ private val actual: NavController
+) : Subject(metadata, actual) { }
+```
+
+### Subject factory
+
+The Subject class should also contain two static fields; a
+[Subject Factory](https://truth.dev/api/latest/com/google/common/truth/Subject.Factory.html)
+and an`assertThat()` shortcut method.
+
+A subject Factory provides most of the functionality of the Subject by allowing
+users to perform all operations provided in the Subject class by passing this
+factory to `about()` methods. E.g:
+
+`assertAbout(navControllers()).that(navController).isGraph(x)` where `isGraph()`
+is a method defined in the Subject class.
+
+The assertThat() shortcut method simply provides a shorthand method for making
+assertions about the Subject without needing a reference to the subject factory.
+i.e., rather than using
+`assertAbout(navControllers()).that(navController).isGraph(x)` users can simply
+use`assertThat(navController).isGraph(x)`.
+
+```kotlin
+companion object {
+ fun navControllers(): Factory<NavControllerSubject, NavController> =
+ Factory<NavControllerSubject, NavController> { metadata, actual ->
+ NavControllerSubject(metadata, actual)
+ }
+
+ @JvmStatic
+ fun assertThat(actual: NavController): NavControllerSubject {
+ return assertAbout(navControllers()).that(actual)
+ }
+}
+```
+
+### Assertion methods
+
+When creating assertion methods for your custom Subject the names of these
+methods should follow the
+[Truth naming convention](https://truth.dev/faq#assertion-naming).
+
+To create assertion methods it is necessary to either delegate to an existing
+assertion method by using `Subject.check()` or to provide your own failure
+strategy by using`failWithActual()` or `failWithoutActual()`.
+
+When using `failWithActual()` the error message will display the`toString()`
+value of the Subject object. Additional information can be added to these error
+messages by using `fact(key, value)` or `simpleFact(value)` where `fact()` will
+be output as a colon separated key, value pair.
+
+```kotlin
+fun isGraph(@IdRes navGraph: Int) {
+ check("graph()").that(actual.graph.id).isEqualTo(navGraph)
+}
+
+// Sample Failure Message
+value of : navController.graph()
+expected : 29340
+but was : 10394
+navController was : {actual.toString() value}
+```
+
+```kotlin
+fun isGraph(@IdRes navGraph: Int) {
+ val actualGraph = actual.graph.id
+ if (actualGraph != navGraph) {
+ failWithoutActual(
+ fact("expected id", navGraph.toString()),
+ fact("but was", actualGraph.toString()),
+ fact("current graph is", actual.graph)
+ )
+ }
+}
+
+// Sample Failure Message
+expected id : 29340
+but was : 10394
+current graph is : {actual.graph.toString() value}
+```
+
+## Testing
+
+When testing expected successful assertions it is enough to simply call the
+assertion with verified successful actual and expected values.
+
+```kotlin
+private lateinit var navController: NavController
+@Before
+fun setUp() {
+ navController = NavController(
+ ApplicationProvider.getApplicationContext()
+ ).apply {
+ navigationProvider += TestNavigator()
+ // R.navigation.testGraph == R.id.test_graph
+ setGraph(R.navigation.test_graph)
+ }
+}
+
+@Test
+fun testGraph() {
+ assertThat(navController).isGraph(R.id.test_graph)
+}
+```
+
+To test that expected failure cases you should use the
+[assertThrows](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:testutils/testutils-truth/src/main/java/androidx/testutils/assertions.kt)
+function from the AndroidX testutils module. The assertions.kt file contains two
+assertThrows functions. The second method, which specifically returns a
+TruthFailureSubject, should be used since it allows for validating additional
+information about the FailureSubject, particularly that it contains specific
+fact messages.
+
+```kotlin
+@Test
+fun testGraphFailure() {
+ with(assertThrows {
+ assertThat(navController).isGraph(R.id.second_test_graph)
+ }) {
+ factValue("expected id").isEqualTo(R.id.second_test_graph.toString())
+ factValue("but was").isEqualTo(navController.graph.id.toString())
+ factValue("current graph is").isEqualTo(navController.graph.toString())
+ }
+}
+```
+
+## Helpful resources
+
+[Truth extension points](https://truth.dev/extension.html)
diff --git a/docs/versioning.md b/docs/versioning.md
new file mode 100644
index 0000000..f1212a3
--- /dev/null
+++ b/docs/versioning.md
@@ -0,0 +1,319 @@
+# Versioning
+
+[TOC]
+
+## Semantic Versioning
+
+Artifacts follow strict semantic versioning. The version for a finalized release
+will follow the format `<major>.<minor>.<bugfix>` with an optional
+`-<alpha|beta><n>` suffix. Internal or nightly releases should use the
+`-SNAPSHOT` suffix to indicate that the release bits are subject to change.
+
+Also check out the [Versioning FAQ](faq.md#version).
+
+### Major (`x.0.0`) {#major}
+
+An artifact's major version indicates a guaranteed forward-compatibility window.
+For example, a developer could update an artifact versioned `2.0.0` to `2.7.3`
+without taking any additional action.
+
+#### When to increment
+
+An artifact *must* increment its major version number in response to breaking
+changes in binary or behavioral compatibility within the library itself _or_ in
+response to breaking changes within a dependency.
+
+For example, if an artifact updates a SemVer-type dependency from `1.0.0` to
+`2.0.0` then the artifact must also bump its own major version number.
+
+An artifact *may in rare cases* increment its major version number to indicate
+an important but non-breaking change in the library. Note, however, that the
+SemVer implications of incrementing the major version are the same as a breaking
+change -- dependent projects _must_ assume the major version change is breaking
+and update their dependency specifications.
+
+#### Ecosystem implications
+
+When an artifact increases its major version, _all_ artifacts that depended on
+the previous major version are no longer considered compatible and must
+explicitly migrate to depend on the new major version.
+
+As a result, if the library ecosystem is slow to adopt a new major version of an
+artifact then developers may end up in a situation where they cannot update an
+artifact because they depend on a library that has not yet adopted the new major
+version.
+
+For this reason, we *strongly* recommend against increasing the major version of
+a “core” artifact that is depended upon by other libraries. “Leaf” artifacts --
+those that apps depend upon directly and are not used by other libraries -- have
+a much easier time increasing their major version.
+
+#### Process requirements
+
+If the artifact has dependencies within Jetpack, owners *must* complete the
+assessment before implementing any breaking changes to binary or behavioral
+compatibility.
+
+Otherwise, owners are *strongly recommended* to complete the assessment before
+implementing any breaking changes to binary or behavioral compatibility, as such
+changes may negatively impact downstream clients in Android git or Google's
+repository. These clients are not part of our pre-submit workflow, but filling
+out the assessment will provide insight into how they will be affected by a
+major version change.
+
+### Minor (`1.x.0`) {#minor}
+
+Minor indicates compatible public API changes. This number is incremented when
+APIs are added, including the addition of `@Deprecated` annotations. Binary
+compatibility must be preserved between minor version changes.
+
+#### Moving between minor versions:
+
+* A change in the minor revision indicates the addition of binary-compatible
+ APIs. Libraries **must** increment their minor revision when adding APIs.
+ Dependent libraries are not required to update their minimum required
+ version unless they depend on newly-added APIs.
+
+### Bugfix (`1.0.x`) {#bugfix}
+
+Bugfix indicates internal changes to address broken behavior. Care should be
+taken to ensure that existing clients are not broken, including clients that may
+have been working around long-standing broken behavior.
+
+#### Moving between bugfix versions:
+
+* A change in the bugfix revision indicates changes in behavior to fix bugs.
+ The API surface does not change. Changes to the bugfix version may *only*
+ occur in a release branch. The bugfix revision must always be `.0` in a
+ development branch.
+
+### Pre-release suffixes {#pre-release-suffix}
+
+The pre-release suffix indicates stability and feature completeness of a
+release. A typical release will begin as alpha, move to beta after acting on
+feedback from internal and external clients, move to release candidate for final
+verification, and ultimately move to a finalized build.
+
+Alpha, beta, and release candidate releases are versioned sequentially using a
+leading zero (ex. alpha01, beta11, rc01) for compatibility with the
+lexicographic ordering of versions used by SemVer.
+
+### Snapshot {#snapshot}
+
+Snapshot releases are whatever exists at tip-of-tree. They are only subject to
+the constraints placed on the average commit. Depending on when it's cut, a
+snapshot may even be binary-identical to an alpha, beta, or stable release.
+
+Versioning policies are enforced by the following Gradle tasks:
+
+`checkApi`: ensures that changes to public API are intentional and tracked,
+asking the developer to explicitly run updateApi (see below) if any changes are
+detected
+
+`checkApiRelease`: verifies that API changes between previously released and
+currently under-development versions conform to semantic versioning guarantees
+
+`updateApi`: commits API changes to source control in such a way that they can
+be reviewed in pre-submit via Gerrit API+1 and reviewed in post-submit by API
+Council
+
+`SNAPSHOT`: is automatically added to the version string for all builds that
+occur outside the build server for release branches (ex. ub-androidx-release).
+Local release builds may be forced by passing -Prelease to the Gradle command
+line.
+
+## Picking the right version {#picking-the-right-version}
+
+AndroidX follows [Strict Semantic Versioning](https://semver.org), which means
+that the version code is strongly tied to the API surface. A full version
+consists of revision numbers for major, minor, and bugfix as well as a
+pre-release stage and revision. Correct versioning is, for the most part,
+automatically enforced; however, please check for the following:
+
+### Initial version {#initial-version}
+
+If your library is brand new, your version should start at 1.0.0, e.g.
+`1.0.0-alpha01`.
+
+The initial release within a new version always starts at `alpha01`. Note the
+two-digit revision code, which allows us to do up to 99 revisions within a
+pre-release stage.
+
+### Pre-release stages
+
+A single version will typically move through several revisions within each of
+the pre-release stages: alpha, beta, rc, and stable. Subsequent revisions within
+a stage (ex. alpha, beta) are incremented by 1, ex. `alpha01` is followed by
+`alpha02` with no gaps.
+
+### Moving between pre-release stages and revisions
+
+Libraries are expected to go through a number of pre-release stages within a
+version prior to release, with stricter requirements at each stage to ensure a
+high-quality stable release. The owner for a library should typically submit a
+CL to update the stage or revision when they are ready to perform a public
+release.
+
+Libraries are expected to allow >= 2 weeks per pre-release stage. This 'soaking
+period' gives developers time to try/use each version, find bugs, and ensure a
+quality stable release. Therefore, at minimum:
+
+- An `alpha` version must be publically available for 2 weeks before releasing
+ a public `beta`
+- A `beta` version must be publically available for 2 weeks before releasing
+ an public `rc`
+- A `rc` version must be publically available fore 2 weeks before releasing a
+ public stable version
+
+Your library must meet the following criteria to move your public release to
+each stage:
+
+### Alpha {#alpha}
+
+Alpha releases are expected to be functionally stable, but may have unstable API
+surface or incomplete features. Typically, alphas have not gone through API
+Council review but are expected to have performed a minimum level of validation.
+
+#### Within the `alphaXX` cycle
+
+* API surface
+ * Prior to `alpha01` release, API tracking **must** be enabled (either
+ `publish=true` or create an `api` directory) and remain enabled
+ * May add/remove APIs within `alpha` cycle, but deprecate/remove cycle is
+ strongly recommended.
+* Testing
+ * All changes **should** be accompanied by a `Test:` stanza
+ * All pre-submit and post-submit tests are passing
+ * Flaky or failing tests **must** be suppressed or fixed within one day
+ (if affecting pre-submit) or three days (if affecting post-submit)
+
+### Beta {#beta}
+
+Beta releases are ready for production use but may contain bugs. They are
+expected to be functionally stable and have highly-stable, feature-complete API
+surface. APIs should have been reviewed by API Council at this stage, and new
+APIs may only be added with approval by API Council. Tests must have 100%
+coverage of public API surface and translations must be 100% complete.
+
+#### Checklist for moving to `beta01`
+
+* API surface
+ * Entire API surface has been reviewed by API Council
+ * All APIs from alpha undergoing deprecate/remove cycle must be removed
+* Testing
+ * All public APIs are tested
+ * All pre-submit and post-submit tests are enabled (e.g. all suppressions
+ are removed) and passing
+ * Your library passes `./gradlew library:checkReleaseReady`
+* No experimental features (e.g. `@UseExperimental`) may be used
+* All dependencies are `beta`, `rc`, or stable
+* Be able to answer the question "How will developers test their apps against
+ your library?"
+ * Ideally, this is an integration app with automated tests that cover the
+ main features of the library and/or a `-testing` artifact as seen in
+ other Jetpack libraries
+
+#### Within the `betaXX` cycle
+
+* API surface
+ * New APIs discouraged unless P0 or P1 (ship-blocking)
+ * May not remove `@Experimental` from experimental APIs, see previous item
+ regarding new APIs
+ * No API removals allowed
+
+### RC {#rc}
+
+Release candidates are expected to be nearly-identical to the final release, but
+may contain critical last-minute fixes for issues found during integration
+testing.
+
+#### Checklist for moving to `rc01`
+
+* All previous checklists still apply
+* Release branch, e.g. `androidx-<group_id>-release`, is created
+* API surface
+ * Any API changes from `beta` cycle are reviewed by API Council
+* No **known** P0 or P1 (ship-blocking) issues
+* All dependencies are `rc` or stable
+
+#### Within the `rcXX` cycle
+
+* Ship-blocking bug fixes only
+* All changes must have corresponding tests
+* No API changes allowed
+
+### Stable {#stable}
+
+Final releases are well-tested, both by internal tests and external clients, and
+their API surface is reviewed and finalized. While APIs may be deprecated and
+removed in future versions, any APIs added at this stage must remain for at
+least a year.
+
+#### Checklist for moving to stable
+
+* Identical to a previously released `rcXX` (note that this means any bugs
+ that result in a new release candidate will necessarily delay your stable
+ release by a minimum of two weeks)
+* No changes of any kind allowed
+* All dependencies are stable
+
+## Updating your version {#update}
+
+A few notes about version updates:
+
+- The version of your library listed in `androidx-master-dev` should *always*
+ be higher than the version publically available on Google Maven. This allows
+ us to do proper version tracking and API tracking.
+
+- Version increments must be done before the CL cutoff date (aka the build cut
+ date).
+
+- **Increments to the next stability suffix** (like `alpha` to `beta`) should
+ be handled by the library owner, with the Jetpack TPM (nickanthony@) CC'd
+ for API+1.
+
+- Version increments in release branches will need to follow the guide
+ [How to update your version on a release branch](release_branches.md#update-your-version)
+
+- When you're ready for `rc01`, the increment to `rc01` should be done in
+ `androidx-master-dev` and then your release branch should be snapped to that
+ build. See the guide [Snap your release branch](release_branches.md#snap) on
+ how to do this. After the release branch is snapped to that build, you will
+ need to update your version in `androidx-master-dev` to `alpha01` of the
+ next minor (or major) version.
+
+### Bi-weekly batched releases (every 2 weeks)
+
+If you participate in a bi-weekly (every 2 weeks) batched release, the Jetpack
+TPM will increment versions for you the day after the build cut deadline. The
+increments are defaulted to increments within the same pre-release suffix.
+
+For example, if you are releasing `1.1.0-alpha04`, the day after the build cut,
+the TPM will increment the version to `1.1.0-alpha05` for the next release.
+
+### How to update your version
+
+1. Update the version listed in
+ `frameworks/support/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt`
+1. Run `./gradlew <your-lib>:updateApi`. This will create an API txt file for
+ the new version of your library.
+1. Verify changes with `./gradlew checkApi verifyDependencyVersions`.
+1. Commit these change as one commit.
+1. Upload these changes to Gerrit for review.
+
+An example of a version bump can be found here:
+[aosp/833800](https://android-review.googlesource.com/c/platform/frameworks/support/+/833800)
+
+## `-ktx` Modules {#ktx}
+
+Kotlin Extension modules (`-ktx`) for regular Java modules follow the same
+requirements, but with one exception. They must match the version of the Java
+module that they extend.
+
+For example, let's say you are developing a java library
+`androidx.foo:foo-bar:1.1.0-alpha01` and you want to add a kotlin extension
+module `androidx.foo:foo-bar-ktx` module. Your new `androidx.foo:foo-bar-ktx`
+module will start at version `1.1.0-alpha01` instead of `1.0.0-alpha01`.
+
+If your `androidx.foo:foo-bar` module was in version `1.0.0-alpha06`, then the
+kotlin extension module would start in version `1.0.0-alpha06`.
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 77b8b66..7e1ec2e 100644
--- a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
+++ b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
@@ -684,6 +684,22 @@
exif.saveAttributes();
exif = new ExifInterface(imageFile.getAbsolutePath());
assertEquals(currentTimeStamp - expectedDatetimeOffsetLongValue, (long) exif.getDateTime());
+
+ // Test that setting null throws NPE
+ try {
+ exif.setDateTime(null);
+ fail();
+ } catch (NullPointerException e) {
+ // Expected
+ }
+
+ // Test that setting negative value throws IAE
+ try {
+ exif.setDateTime(-1L);
+ fail();
+ } catch (IllegalArgumentException e) {
+ // Expected
+ }
}
/**
@@ -757,29 +773,80 @@
assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
}
- // TODO: Add tests for other variations (e.g. single/double digit number strings)
@Test
@LargeTest
- public void testParsingSubsec() throws IOException {
+ public void testSubsec() throws IOException {
File imageFile = getFileFromExternalDir(JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT);
ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
- exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, /* 0ms */ "000000");
+
+ // Set initial value to 0
+ exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, /* 0ms */ "000");
exif.saveAttributes();
+ assertEquals("000", exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME));
long currentDateTimeValue = exif.getDateTime();
- // Check that TAG_SUBSEC_TIME values starting with zero are supported.
- // Note: getDateTime() supports only up to 1/1000th of a second.
- exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, /* 1ms */ "001000");
- exif.saveAttributes();
- assertEquals(currentDateTimeValue + 1, (long) exif.getDateTime());
-
- exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, /* 10ms */ "010000");
- exif.saveAttributes();
- assertEquals(currentDateTimeValue + 10, (long) exif.getDateTime());
-
- exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, /* 100ms */ "100000");
+ // Test that single and double-digit values are set properly.
+ // Note that since SubSecTime tag records fractions of a second, a single-digit value
+ // should be counted as the first decimal value, which is why "1" becomes 100ms and "11"
+ // becomes 110ms.
+ String oneDigitSubSec = "1";
+ exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, oneDigitSubSec);
exif.saveAttributes();
assertEquals(currentDateTimeValue + 100, (long) exif.getDateTime());
+ assertEquals(oneDigitSubSec, exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME));
+
+ String twoDigitSubSec1 = "01";
+ exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, twoDigitSubSec1);
+ exif.saveAttributes();
+ assertEquals(currentDateTimeValue + 10, (long) exif.getDateTime());
+ assertEquals(twoDigitSubSec1, exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME));
+
+ String twoDigitSubSec2 = "11";
+ exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, twoDigitSubSec2);
+ exif.saveAttributes();
+ assertEquals(currentDateTimeValue + 110, (long) exif.getDateTime());
+ assertEquals(twoDigitSubSec2, exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME));
+
+ // Test that 3-digit values are set properly.
+ String hundredMs = "100";
+ exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, hundredMs);
+ exif.saveAttributes();
+ assertEquals(currentDateTimeValue + 100, (long) exif.getDateTime());
+ assertEquals(hundredMs, exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME));
+
+ // Test that values starting with zero are also supported.
+ String oneMsStartingWithZeroes = "001";
+ exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, oneMsStartingWithZeroes);
+ exif.saveAttributes();
+ assertEquals(currentDateTimeValue + 1, (long) exif.getDateTime());
+ assertEquals(oneMsStartingWithZeroes, exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME));
+
+ String tenMsStartingWithZero = "010";
+ exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, tenMsStartingWithZero);
+ exif.saveAttributes();
+ assertEquals(currentDateTimeValue + 10, (long) exif.getDateTime());
+ assertEquals(tenMsStartingWithZero, exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME));
+
+ // Test that values with more than three digits are set properly. getAttribute() should
+ // return the whole string, but getDateTime() should only add the first three digits
+ // because it supports only up to 1/1000th of a second.
+ String fourDigitSubSec = "1234";
+ exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, fourDigitSubSec);
+ exif.saveAttributes();
+ assertEquals(currentDateTimeValue + 123, (long) exif.getDateTime());
+ assertEquals(fourDigitSubSec, exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME));
+
+ String fiveDigitSubSec = "23456";
+ exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, fiveDigitSubSec);
+ exif.saveAttributes();
+ assertEquals(currentDateTimeValue + 234, (long) exif.getDateTime());
+ assertEquals(fiveDigitSubSec, exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME));
+
+ String sixDigitSubSec = "345678";
+ exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, sixDigitSubSec);
+ exif.saveAttributes();
+ assertEquals(currentDateTimeValue + 345, (long) exif.getDateTime());
+ assertEquals(sixDigitSubSec, exif.getAttribute(ExifInterface.TAG_SUBSEC_TIME));
}
@Test
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 76d6f81..4099177 100644
--- a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
+++ b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
@@ -5151,9 +5151,21 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public void setDateTime(@NonNull Long timeStamp) {
- long sub = timeStamp % 1000;
+ if (timeStamp == null) {
+ throw new NullPointerException("Timestamp should not be null.");
+ }
+
+ if (timeStamp < 0) {
+ throw new IllegalArgumentException("Timestamp should a positive value.");
+ }
+
+ long subsec = timeStamp % 1000;
+ String subsecString = Long.toString(subsec);
+ for (int i = subsecString.length(); i < 3; i++) {
+ subsecString = "0" + subsecString;
+ }
setAttribute(TAG_DATETIME, sFormatterPrimary.format(new Date(timeStamp)));
- setAttribute(TAG_SUBSEC_TIME, Long.toString(sub));
+ setAttribute(TAG_SUBSEC_TIME, subsecString);
}
/**
diff --git a/fragment/fragment-testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.kt b/fragment/fragment-testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.kt
index 0674e06..a2735f9 100644
--- a/fragment/fragment-testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.kt
+++ b/fragment/fragment-testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.kt
@@ -228,7 +228,7 @@
* If your testing Fragment has a dependency to specific theme such as `Theme.AppCompat`,
* use the theme ID parameter in [launch] method.
*
- * @param <F> The Fragment class being tested
+ * @param F The Fragment class being tested
*
* @see ActivityScenario a scenario API for Activity
*/
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/OptionsMenuFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/OptionsMenuFragmentTest.kt
index 1775a46..1776faf 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/OptionsMenuFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/OptionsMenuFragmentTest.kt
@@ -42,7 +42,7 @@
@Test
fun fragmentWithOptionsMenu() {
activityRule.setContentView(R.layout.simple_container)
- val fragment = OptionsMenuFragment()
+ val fragment = MenuFragment()
val fm = activityRule.activity.supportFragmentManager
fm.beginTransaction()
.add(R.id.fragmentContainer, fragment)
@@ -112,6 +112,30 @@
}
@Test
+ fun childFragmentWithPrepareOptionsMenuParentMenuVisibilityFalse() {
+ activityRule.setContentView(R.layout.simple_container)
+ val parent = ParentOptionsMenuFragment()
+ val fm = activityRule.activity.supportFragmentManager
+
+ parent.setMenuVisibility(false)
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, parent, "parent")
+ .commit()
+ activityRule.executePendingTransactions()
+
+ assertWithMessage("Fragment should not have an options menu")
+ .that(parent.hasOptionsMenu()).isFalse()
+ assertWithMessage("Child fragment should have an options menu")
+ .that(parent.childFragmentManager.checkForMenus()).isTrue()
+
+ activityRule.runOnUiThread {
+ assertWithMessage("child fragment onCreateOptions menu was not called")
+ .that(parent.childFragment.onPrepareOptionsMenuCountDownLatch.count)
+ .isEqualTo(1)
+ }
+ }
+
+ @Test
fun parentAndChildFragmentWithOptionsMenu() {
activityRule.setContentView(R.layout.simple_container)
val parent = ParentOptionsMenuFragment(true)
@@ -165,7 +189,7 @@
activityRule.executePendingTransactions()
parent.childFragmentManager.beginTransaction()
- .add(R.id.fragmentContainer2, OptionsMenuFragment())
+ .add(R.id.fragmentContainer2, MenuFragment())
.commit()
activityRule.executePendingTransactions()
@@ -175,8 +199,9 @@
.that(parent.mChildFragmentManager.checkForMenus()).isTrue()
}
- class OptionsMenuFragment : StrictViewFragment(R.layout.fragment_a) {
+ class MenuFragment : StrictViewFragment(R.layout.fragment_a) {
val onCreateOptionsMenuCountDownLatch = CountDownLatch(1)
+ val onPrepareOptionsMenuCountDownLatch = CountDownLatch(1)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -188,12 +213,17 @@
onCreateOptionsMenuCountDownLatch.countDown()
inflater.inflate(R.menu.example_menu, menu)
}
+
+ override fun onPrepareOptionsMenu(menu: Menu) {
+ super.onPrepareOptionsMenu(menu)
+ onPrepareOptionsMenuCountDownLatch.countDown()
+ }
}
class ParentOptionsMenuFragment(
val createMenu: Boolean = false
) : StrictViewFragment(R.layout.double_container) {
- val childFragment = OptionsMenuFragment()
+ val childFragment = MenuFragment()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index 2cc8680..f3e9d37 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -3193,7 +3193,7 @@
boolean show = false;
for (Fragment f : mFragmentStore.getFragments()) {
if (f != null) {
- if (f.performPrepareOptionsMenu(menu)) {
+ if (isParentMenuVisible(f) && f.performPrepareOptionsMenu(menu)) {
show = true;
}
}
diff --git a/inspection/inspection-gradle-plugin/build.gradle b/inspection/inspection-gradle-plugin/build.gradle
index aff9915..b09f862 100644
--- a/inspection/inspection-gradle-plugin/build.gradle
+++ b/inspection/inspection-gradle-plugin/build.gradle
@@ -36,7 +36,7 @@
implementation(AGP_STABLE)
implementation(KOTLIN_STDLIB)
implementation("gradle.plugin.com.google.protobuf:protobuf-gradle-plugin:0.8.13")
- implementation("org.anarres.jarjar:jarjar-gradle:1.0.1")
+ implementation("com.github.jengelman.gradle.plugins:shadow:5.2.0")
testImplementation(project(":internal-testutils-gradle-plugin"))
testImplementation gradleTestKit()
diff --git a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/DexInspectorTask.kt b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/DexInspectorTask.kt
index e8370d5..b10b84b 100644
--- a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/DexInspectorTask.kt
+++ b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/DexInspectorTask.kt
@@ -19,7 +19,6 @@
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.api.BaseVariant
import com.android.build.gradle.api.LibraryVariant
-import org.anarres.gradle.plugin.jarjar.JarjarTask
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.file.ConfigurableFileCollection
@@ -30,6 +29,7 @@
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
+import org.gradle.api.tasks.bundling.Jar
import java.io.File
abstract class DexInspectorTask : DefaultTask() {
@@ -73,13 +73,13 @@
fun Project.registerDexInspectorTask(
variant: BaseVariant,
extension: BaseExtension,
- jarJar: TaskProvider<JarjarTask>
+ jar: TaskProvider<out Jar>
): TaskProvider<DexInspectorTask> {
return tasks.register(variant.taskName("dexInspector"), DexInspectorTask::class.java) {
it.setDx(extension.sdkDirectory, extension.buildToolsVersion)
- it.jars.from(jarJar.get().destinationPath)
+ it.jars.from(jar.get().destinationDirectory)
val out = File(taskWorkingDir(variant, "dexedInspector"), "${project.name}.jar")
it.outputFile.set(out)
- it.dependsOn(jarJar)
+ it.dependsOn(jar)
}
}
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 7751681..e5a1eba 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
@@ -52,8 +52,8 @@
if (variant.name == "release") {
foundReleaseVariant = true
val unzip = project.registerUnzipTask(variant)
- val jarJar = project.registerJarJarDependenciesTask(variant, unzip)
- dexTask = project.registerDexInspectorTask(variant, libExtension, jarJar)
+ val shadowJar = project.registerShadowDependenciesTask(variant, unzip)
+ dexTask = project.registerDexInspectorTask(variant, libExtension, shadowJar)
}
}
libExtension.sourceSets.findByName("main")!!.resources.srcDirs(
diff --git a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/JarJarDependenciesTask.kt b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/ShadowDependenciesTask.kt
similarity index 84%
rename from inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/JarJarDependenciesTask.kt
rename to inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/ShadowDependenciesTask.kt
index 33b6ca2b..14893cb 100644
--- a/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/JarJarDependenciesTask.kt
+++ b/inspection/inspection-gradle-plugin/src/main/kotlin/androidx/inspection/gradle/ShadowDependenciesTask.kt
@@ -17,7 +17,7 @@
package androidx.inspection.gradle
import com.android.build.gradle.api.BaseVariant
-import org.anarres.gradle.plugin.jarjar.JarjarTask
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.gradle.api.Project
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.TaskProvider
@@ -27,29 +27,29 @@
// variant.taskName relies on @ExperimentalStdlibApi api
@ExperimentalStdlibApi
-fun Project.registerJarJarDependenciesTask(
+fun Project.registerShadowDependenciesTask(
variant: BaseVariant,
zipTask: TaskProvider<Copy>
-): TaskProvider<JarjarTask> {
+): TaskProvider<ShadowJar> {
val uberJar = registerUberJarTask(variant)
return tasks.register(
- variant.taskName("jarJarDependencies"),
- JarjarTask::class.java
+ variant.taskName("shadowDependencies"),
+ ShadowJar::class.java
) {
it.dependsOn(uberJar)
val fileTree = project.fileTree(zipTask.get().destinationDir)
fileTree.include("**/*.jar")
it.from(fileTree)
- it.destinationDir = taskWorkingDir(variant, "jarJar")
- it.destinationName = "${project.name}-shadowed.jar"
+ it.destinationDirectory.set(taskWorkingDir(variant, "shadowedJar"))
+ it.archiveBaseName.set("${project.name}-shadowed")
it.dependsOn(zipTask)
val prefix = "deps.${project.name.replace('-', '.')}"
it.doFirst {
- val task = it as JarjarTask
+ val task = it as ShadowJar
val input = uberJar.get().outputs.files
task.from(input)
input.extractPackageNames().forEach { packageName ->
- task.classRename("$packageName.**", "$prefix.$packageName.@1")
+ task.relocate(packageName, "$prefix.$packageName")
}
}
}
diff --git a/jetifier/jetifier/processor/src/test/kotlin/com/android/tools/build/jetifier/processor/transform/pom/PomRewriteInZipTest.kt b/jetifier/jetifier/processor/src/test/kotlin/com/android/tools/build/jetifier/processor/transform/pom/PomRewriteInZipTest.kt
index df0666f..71dafe2 100644
--- a/jetifier/jetifier/processor/src/test/kotlin/com/android/tools/build/jetifier/processor/transform/pom/PomRewriteInZipTest.kt
+++ b/jetifier/jetifier/processor/src/test/kotlin/com/android/tools/build/jetifier/processor/transform/pom/PomRewriteInZipTest.kt
@@ -64,9 +64,9 @@
val inputFile = File(javaClass.getResource(inputZipPath).file)
- @Suppress("DEPRECATION")
+ @Suppress("DEPRECATION") // b/174695914
val tempDir = createTempDir()
- @Suppress("DEPRECATION")
+ @Suppress("DEPRECATION") // b/174695914
val expectedFile = File(createTempDir(), "test.zip")
@Suppress("deprecation")
@@ -103,9 +103,9 @@
val inputFile = File(javaClass.getResource(inputZipPath).file)
- @Suppress("DEPRECATION")
+ @Suppress("DEPRECATION") // b/174695914
val tempDir = createTempDir()
- @Suppress("DEPRECATION")
+ @Suppress("DEPRECATION") // b/174695914
val expectedFile = File(createTempDir(), "test.zip")
@Suppress("deprecation")
diff --git a/jetifier/jetifier/standalone/src/main/kotlin/com/android/tools/build/jetifier/standalone/Main.kt b/jetifier/jetifier/standalone/src/main/kotlin/com/android/tools/build/jetifier/standalone/Main.kt
index 9e7ec3d..af71151 100644
--- a/jetifier/jetifier/standalone/src/main/kotlin/com/android/tools/build/jetifier/standalone/Main.kt
+++ b/jetifier/jetifier/standalone/src/main/kotlin/com/android/tools/build/jetifier/standalone/Main.kt
@@ -166,7 +166,7 @@
val fileMappings = mutableSetOf<FileMapping>()
if (rebuildTopOfTree) {
- @Suppress("DEPRECATION")
+ @Suppress("DEPRECATION") // b/174695914
val tempFile = createTempFile(suffix = "zip")
fileMappings.add(FileMapping(input, tempFile))
} else {
diff --git a/leanback/leanback-paging/build.gradle b/leanback/leanback-paging/build.gradle
index 7a7e046..63486ac 100644
--- a/leanback/leanback-paging/build.gradle
+++ b/leanback/leanback-paging/build.gradle
@@ -12,8 +12,8 @@
dependencies {
api("androidx.annotation:annotation:1.1.0")
- api(project(":leanback:leanback"))
- api("androidx.paging:paging-runtime:3.0.0-alpha09")
+ api("androidx.leanback:leanback:1.1.0-beta01")
+ api(project(":paging:paging-runtime"))
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
androidTestImplementation(ANDROIDX_TEST_CORE)
diff --git a/leanback/leanback-paging/src/androidTest/java/androidx/leanback/paging/PagingDataAdapterTest.kt b/leanback/leanback-paging/src/androidTest/java/androidx/leanback/paging/PagingDataAdapterTest.kt
index 85e2137..7e4b192 100644
--- a/leanback/leanback-paging/src/androidTest/java/androidx/leanback/paging/PagingDataAdapterTest.kt
+++ b/leanback/leanback-paging/src/androidTest/java/androidx/leanback/paging/PagingDataAdapterTest.kt
@@ -19,14 +19,12 @@
import androidx.paging.CombinedLoadStates
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadState
-import androidx.paging.LoadType
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.TestPagingSource
import androidx.paging.assertEvents
import androidx.paging.localLoadStatesOf
-import androidx.paging.toCombinedLoadStatesLocal
import androidx.recyclerview.widget.DiffUtil
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -139,15 +137,18 @@
// empty previous list.
assertEvents(
listOf(
- LoadType.REFRESH to LoadState.Loading,
- LoadType.REFRESH to LoadState.NotLoading(endOfPaginationReached = false)
- ).toCombinedLoadStatesLocal(),
+ localLoadStatesOf(),
+ localLoadStatesOf(refreshLocal = LoadState.Loading),
+ localLoadStatesOf(
+ refreshLocal = LoadState.NotLoading(endOfPaginationReached = false)
+ ),
+ ),
loadEvents
)
loadEvents.clear()
job.cancel()
- pagingDataAdapter.submitData(TestLifecycleOwner().lifecycle, PagingData.empty<Int>())
+ pagingDataAdapter.submitData(TestLifecycleOwner().lifecycle, PagingData.empty())
advanceUntilIdle()
// Assert that all load state updates are sent, even when differ enters fast path for
// empty next list.
diff --git a/leanback/leanback-preference/build.gradle b/leanback/leanback-preference/build.gradle
index caedc44..81faf4c 100644
--- a/leanback/leanback-preference/build.gradle
+++ b/leanback/leanback-preference/build.gradle
@@ -13,7 +13,7 @@
api("androidx.appcompat:appcompat:1.0.0")
api("androidx.recyclerview:recyclerview:1.0.0")
api("androidx.preference:preference:1.1.0")
- api(project(":leanback:leanback"))
+ api("androidx.leanback:leanback:1.1.0-beta01")
}
android {
diff --git a/leanback/leanback-tab/build.gradle b/leanback/leanback-tab/build.gradle
index d58b65a..4481a37 100644
--- a/leanback/leanback-tab/build.gradle
+++ b/leanback/leanback-tab/build.gradle
@@ -46,7 +46,7 @@
androidx {
name = "AndroidX Leanback Tab"
publish = Publish.SNAPSHOT_AND_RELEASE
- mavenVersion = LibraryVersions.LEANBACK
+ mavenVersion = LibraryVersions.LEANBACK_TAB
mavenGroup = LibraryGroups.LEANBACK
inceptionYear = "2020"
description = "This library adds top tab navigation component to be used in TV"
diff --git a/leanback/leanback/api/1.1.0-beta01.txt b/leanback/leanback/api/1.1.0-beta01.txt
index 808452b..d92b77a 100644
--- a/leanback/leanback/api/1.1.0-beta01.txt
+++ b/leanback/leanback/api/1.1.0-beta01.txt
@@ -883,6 +883,7 @@
method @Deprecated public void onCreate(android.os.Bundle!);
method @Deprecated public android.view.View! onCreateView(android.view.LayoutInflater!, android.view.ViewGroup!, android.os.Bundle!);
method @Deprecated public void onDestroy();
+ method @Deprecated public void onDestroyView();
method @Deprecated public void onPause();
method @Deprecated public void onRequestPermissionsResult(int, String![]!, int[]!);
method @Deprecated public void onResume();
diff --git a/leanback/leanback/api/current.txt b/leanback/leanback/api/current.txt
index 808452b..d92b77a 100644
--- a/leanback/leanback/api/current.txt
+++ b/leanback/leanback/api/current.txt
@@ -883,6 +883,7 @@
method @Deprecated public void onCreate(android.os.Bundle!);
method @Deprecated public android.view.View! onCreateView(android.view.LayoutInflater!, android.view.ViewGroup!, android.os.Bundle!);
method @Deprecated public void onDestroy();
+ method @Deprecated public void onDestroyView();
method @Deprecated public void onPause();
method @Deprecated public void onRequestPermissionsResult(int, String![]!, int[]!);
method @Deprecated public void onResume();
diff --git a/leanback/leanback/api/public_plus_experimental_1.1.0-beta01.txt b/leanback/leanback/api/public_plus_experimental_1.1.0-beta01.txt
index 808452b..d92b77a 100644
--- a/leanback/leanback/api/public_plus_experimental_1.1.0-beta01.txt
+++ b/leanback/leanback/api/public_plus_experimental_1.1.0-beta01.txt
@@ -883,6 +883,7 @@
method @Deprecated public void onCreate(android.os.Bundle!);
method @Deprecated public android.view.View! onCreateView(android.view.LayoutInflater!, android.view.ViewGroup!, android.os.Bundle!);
method @Deprecated public void onDestroy();
+ method @Deprecated public void onDestroyView();
method @Deprecated public void onPause();
method @Deprecated public void onRequestPermissionsResult(int, String![]!, int[]!);
method @Deprecated public void onResume();
diff --git a/leanback/leanback/api/public_plus_experimental_current.txt b/leanback/leanback/api/public_plus_experimental_current.txt
index 808452b..d92b77a 100644
--- a/leanback/leanback/api/public_plus_experimental_current.txt
+++ b/leanback/leanback/api/public_plus_experimental_current.txt
@@ -883,6 +883,7 @@
method @Deprecated public void onCreate(android.os.Bundle!);
method @Deprecated public android.view.View! onCreateView(android.view.LayoutInflater!, android.view.ViewGroup!, android.os.Bundle!);
method @Deprecated public void onDestroy();
+ method @Deprecated public void onDestroyView();
method @Deprecated public void onPause();
method @Deprecated public void onRequestPermissionsResult(int, String![]!, int[]!);
method @Deprecated public void onResume();
diff --git a/leanback/leanback/api/restricted_1.1.0-beta01.txt b/leanback/leanback/api/restricted_1.1.0-beta01.txt
index 981b38c..65d5941 100644
--- a/leanback/leanback/api/restricted_1.1.0-beta01.txt
+++ b/leanback/leanback/api/restricted_1.1.0-beta01.txt
@@ -924,6 +924,7 @@
method @Deprecated public void onCreate(android.os.Bundle!);
method @Deprecated public android.view.View! onCreateView(android.view.LayoutInflater!, android.view.ViewGroup!, android.os.Bundle!);
method @Deprecated public void onDestroy();
+ method @Deprecated public void onDestroyView();
method @Deprecated public void onPause();
method @Deprecated public void onRequestPermissionsResult(int, String![]!, int[]!);
method @Deprecated public void onResume();
diff --git a/leanback/leanback/api/restricted_current.txt b/leanback/leanback/api/restricted_current.txt
index 981b38c..65d5941 100644
--- a/leanback/leanback/api/restricted_current.txt
+++ b/leanback/leanback/api/restricted_current.txt
@@ -924,6 +924,7 @@
method @Deprecated public void onCreate(android.os.Bundle!);
method @Deprecated public android.view.View! onCreateView(android.view.LayoutInflater!, android.view.ViewGroup!, android.os.Bundle!);
method @Deprecated public void onDestroy();
+ method @Deprecated public void onDestroyView();
method @Deprecated public void onPause();
method @Deprecated public void onRequestPermissionsResult(int, String![]!, int[]!);
method @Deprecated public void onResume();
diff --git a/leanback/leanback/build.gradle b/leanback/leanback/build.gradle
index df6b059f..8e03560 100644
--- a/leanback/leanback/build.gradle
+++ b/leanback/leanback/build.gradle
@@ -15,7 +15,7 @@
implementation("androidx.collection:collection:1.0.0")
api("androidx.media:media:1.0.0")
api("androidx.fragment:fragment:1.0.0")
- api project(":recyclerview:recyclerview")
+ api("androidx.recyclerview:recyclerview:1.2.0-beta01")
api("androidx.appcompat:appcompat:1.0.0")
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/leanback/leanback/src/androidTest/generatev4.py b/leanback/leanback/src/androidTest/generatev4.py
index c540b2a..d05788b 100755
--- a/leanback/leanback/src/androidTest/generatev4.py
+++ b/leanback/leanback/src/androidTest/generatev4.py
@@ -75,7 +75,8 @@
####### generate XXXFragmentTest classes #######
-testcls = ['Browse', 'GuidedStep', 'VerticalGrid', 'Playback', 'Video', 'Details', 'Rows', 'Headers']
+testcls = ['Browse', 'GuidedStep', 'VerticalGrid', 'Playback', 'Video', 'Details', 'Rows',
+'Headers', 'Search']
for w in testcls:
print "copy {}SupporFragmentTest to {}FragmentTest".format(w, w)
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchFragmentTest.java
new file mode 100644
index 0000000..0565f5f
--- /dev/null
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchFragmentTest.java
@@ -0,0 +1,157 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from SearchSupportFragmentTest.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.leanback.app;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+
+import android.app.Fragment;
+import androidx.leanback.test.R;
+import androidx.leanback.testutils.LeakDetector;
+import androidx.leanback.testutils.PollingCheck;
+import androidx.leanback.widget.ArrayObjectAdapter;
+import androidx.leanback.widget.HeaderItem;
+import androidx.leanback.widget.ListRow;
+import androidx.leanback.widget.ListRowPresenter;
+import androidx.leanback.widget.ObjectAdapter;
+import androidx.leanback.widget.VerticalGridView;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.testutils.AnimationTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@AnimationTest
+@RunWith(AndroidJUnit4.class)
+public class SearchFragmentTest extends SingleFragmentTestBase {
+
+ static final StringPresenter CARD_PRESENTER = new StringPresenter();
+
+ static void loadData(ArrayObjectAdapter adapter, int numRows, int repeatPerRow) {
+ for (int i = 0; i < numRows; ++i) {
+ ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(CARD_PRESENTER);
+ int index = 0;
+ for (int j = 0; j < repeatPerRow; ++j) {
+ listRowAdapter.add("Hello world-" + (index++));
+ listRowAdapter.add("This is a test-" + (index++));
+ listRowAdapter.add("Android TV-" + (index++));
+ listRowAdapter.add("Leanback-" + (index++));
+ listRowAdapter.add("Hello world-" + (index++));
+ listRowAdapter.add("Android TV-" + (index++));
+ listRowAdapter.add("Leanback-" + (index++));
+ listRowAdapter.add("GuidedStepFragment-" + (index++));
+ }
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ adapter.add(new ListRow(header, listRowAdapter));
+ }
+ }
+
+ public static final class F_LeakFragment extends SearchFragment
+ implements SearchFragment.SearchResultProvider {
+ ArrayObjectAdapter mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ loadData(mRowsAdapter, 10, 1);
+ }
+
+ @Override
+ public ObjectAdapter getResultsAdapter() {
+ return mRowsAdapter;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newQuery) {
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ return true;
+ }
+ }
+
+ public static final class EmptyFragment extends Fragment {
+ EditText mEditText;
+
+ @Override
+ public View onCreateView(
+ final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return mEditText = new EditText(container.getContext());
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ // focus IME on the new fragment because there is a memory leak that IME remembers
+ // last editable view, which will cause a false reporting of leaking View.
+ InputMethodManager imm =
+ (InputMethodManager) getActivity()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ mEditText.requestFocus();
+ imm.showSoftInput(mEditText, 0);
+ }
+
+ @Override
+ public void onDestroyView() {
+ mEditText = null;
+ super.onDestroyView();
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) // API 17 retains local Variable
+ @Test
+ public void viewLeakTest() throws Throwable {
+ SingleFragmentTestActivity activity = launchAndWaitActivity(F_LeakFragment.class,
+ 1000);
+
+ VerticalGridView gridView = ((SearchFragment) activity.getTestFragment())
+ .getRowsFragment().getVerticalGridView();
+ LeakDetector leakDetector = new LeakDetector();
+ leakDetector.observeObject(gridView);
+ leakDetector.observeObject(gridView.getRecycledViewPool());
+ for (int i = 0; i < gridView.getChildCount(); i++) {
+ leakDetector.observeObject(gridView.getChildAt(i));
+ }
+ gridView = null;
+ EmptyFragment emptyFragment = new EmptyFragment();
+ activity.getFragmentManager().beginTransaction()
+ .replace(R.id.main_frame, emptyFragment)
+ .addToBackStack("BK")
+ .commit();
+
+ PollingCheck.waitFor(1000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return emptyFragment.isResumed();
+ }
+ });
+ leakDetector.assertNoLeak();
+ }
+}
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchSupportFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchSupportFragmentTest.java
new file mode 100644
index 0000000..446400b
--- /dev/null
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchSupportFragmentTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.leanback.app;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+
+import androidx.fragment.app.Fragment;
+import androidx.leanback.test.R;
+import androidx.leanback.testutils.LeakDetector;
+import androidx.leanback.testutils.PollingCheck;
+import androidx.leanback.widget.ArrayObjectAdapter;
+import androidx.leanback.widget.HeaderItem;
+import androidx.leanback.widget.ListRow;
+import androidx.leanback.widget.ListRowPresenter;
+import androidx.leanback.widget.ObjectAdapter;
+import androidx.leanback.widget.VerticalGridView;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.testutils.AnimationTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@AnimationTest
+@RunWith(AndroidJUnit4.class)
+public class SearchSupportFragmentTest extends SingleSupportFragmentTestBase {
+
+ static final StringPresenter CARD_PRESENTER = new StringPresenter();
+
+ static void loadData(ArrayObjectAdapter adapter, int numRows, int repeatPerRow) {
+ for (int i = 0; i < numRows; ++i) {
+ ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(CARD_PRESENTER);
+ int index = 0;
+ for (int j = 0; j < repeatPerRow; ++j) {
+ listRowAdapter.add("Hello world-" + (index++));
+ listRowAdapter.add("This is a test-" + (index++));
+ listRowAdapter.add("Android TV-" + (index++));
+ listRowAdapter.add("Leanback-" + (index++));
+ listRowAdapter.add("Hello world-" + (index++));
+ listRowAdapter.add("Android TV-" + (index++));
+ listRowAdapter.add("Leanback-" + (index++));
+ listRowAdapter.add("GuidedStepSupportFragment-" + (index++));
+ }
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ adapter.add(new ListRow(header, listRowAdapter));
+ }
+ }
+
+ public static final class F_LeakFragment extends SearchSupportFragment
+ implements SearchSupportFragment.SearchResultProvider {
+ ArrayObjectAdapter mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ loadData(mRowsAdapter, 10, 1);
+ }
+
+ @Override
+ public ObjectAdapter getResultsAdapter() {
+ return mRowsAdapter;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newQuery) {
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ return true;
+ }
+ }
+
+ public static final class EmptyFragment extends Fragment {
+ EditText mEditText;
+
+ @Override
+ public View onCreateView(
+ final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return mEditText = new EditText(container.getContext());
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ // focus IME on the new fragment because there is a memory leak that IME remembers
+ // last editable view, which will cause a false reporting of leaking View.
+ InputMethodManager imm =
+ (InputMethodManager) getActivity()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ mEditText.requestFocus();
+ imm.showSoftInput(mEditText, 0);
+ }
+
+ @Override
+ public void onDestroyView() {
+ mEditText = null;
+ super.onDestroyView();
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) // API 17 retains local Variable
+ @Test
+ public void viewLeakTest() throws Throwable {
+ SingleSupportFragmentTestActivity activity = launchAndWaitActivity(F_LeakFragment.class,
+ 1000);
+
+ VerticalGridView gridView = ((SearchSupportFragment) activity.getTestFragment())
+ .getRowsSupportFragment().getVerticalGridView();
+ LeakDetector leakDetector = new LeakDetector();
+ leakDetector.observeObject(gridView);
+ leakDetector.observeObject(gridView.getRecycledViewPool());
+ for (int i = 0; i < gridView.getChildCount(); i++) {
+ leakDetector.observeObject(gridView.getChildAt(i));
+ }
+ gridView = null;
+ EmptyFragment emptyFragment = new EmptyFragment();
+ activity.getSupportFragmentManager().beginTransaction()
+ .replace(R.id.main_frame, emptyFragment)
+ .addToBackStack("BK")
+ .commit();
+
+ PollingCheck.waitFor(1000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return emptyFragment.isResumed();
+ }
+ });
+ leakDetector.assertNoLeak();
+ }
+}
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/SearchFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/SearchFragment.java
index 21cc610..644405a 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/SearchFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/SearchFragment.java
@@ -424,6 +424,13 @@
}
@Override
+ public void onDestroyView() {
+ mSearchBar = null;
+ mRowsFragment = null;
+ super.onDestroyView();
+ }
+
+ @Override
public void onDestroy() {
releaseAdapter();
super.onDestroy();
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/SearchSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/SearchSupportFragment.java
index b28c4ec..124da0b 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/SearchSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/SearchSupportFragment.java
@@ -419,6 +419,13 @@
}
@Override
+ public void onDestroyView() {
+ mSearchBar = null;
+ mRowsSupportFragment = null;
+ super.onDestroyView();
+ }
+
+ @Override
public void onDestroy() {
releaseAdapter();
super.onDestroy();
diff --git a/lifecycle/lifecycle-service/build.gradle b/lifecycle/lifecycle-service/build.gradle
index 1f4f5dff..2b81a01 100644
--- a/lifecycle/lifecycle-service/build.gradle
+++ b/lifecycle/lifecycle-service/build.gradle
@@ -31,7 +31,7 @@
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
androidTestImplementation(ANDROIDX_TEST_CORE)
androidTestImplementation(ANDROIDX_TEST_RUNNER)
- androidTestImplementation("androidx.legacy:legacy-support-core-utils:1.0.0")
+ androidTestImplementation("androidx.localbroadcastmanager:localbroadcastmanager:1.0.0")
}
androidx {
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleTest.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleTest.kt
index 314443b..8e3a3fb 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleTest.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleTest.kt
@@ -16,6 +16,7 @@
package androidx.lifecycle.viewmodel.savedstate
+import android.os.Bundle
import androidx.annotation.MainThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.SavedStateHandle
@@ -169,9 +170,11 @@
fun testKeySet() {
val accessor = SavedStateHandle()
accessor.set("s", "pb")
+ accessor.getLiveData<String>("no value ld")
accessor.getLiveData<String>("ld").value = "a"
- assertThat(accessor.keys().size).isEqualTo(2)
- assertThat(accessor.keys()).containsExactly("s", "ld")
+ accessor.setSavedStateProvider("provider") { Bundle() }
+ assertThat(accessor.keys().size).isEqualTo(4)
+ assertThat(accessor.keys()).containsExactly("s", "ld", "provider", "no value ld")
}
@MainThread
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.java b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.java
index 146eb52..af17aa1 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.java
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.java
@@ -32,8 +32,8 @@
import java.io.Serializable;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@@ -217,11 +217,17 @@
/**
* Returns all keys contained in this {@link SavedStateHandle}
+ * <p>
+ * Returned set contains all keys: keys used to get LiveData-s, to set SavedStateProviders and
+ * keys used in regular {@link #set(String, Object)}.
*/
@MainThread
@NonNull
public Set<String> keys() {
- return Collections.unmodifiableSet(mRegular.keySet());
+ HashSet<String> allKeys = new HashSet<>(mRegular.keySet());
+ allKeys.addAll(mSavedStateProviders.keySet());
+ allKeys.addAll(mLiveDatas.keySet());
+ return allKeys;
}
/**
diff --git a/media/media/api/current.txt b/media/media/api/current.txt
index 983f283..ff8752c 100644
--- a/media/media/api/current.txt
+++ b/media/media/api/current.txt
@@ -446,6 +446,7 @@
field public static final long ACTION_REWIND = 8L; // 0x8L
field public static final long ACTION_SEEK_TO = 256L; // 0x100L
field public static final long ACTION_SET_CAPTIONING_ENABLED = 1048576L; // 0x100000L
+ field public static final long ACTION_SET_PLAYBACK_SPEED = 4194304L; // 0x400000L
field public static final long ACTION_SET_RATING = 128L; // 0x80L
field public static final long ACTION_SET_REPEAT_MODE = 262144L; // 0x40000L
field public static final long ACTION_SET_SHUFFLE_MODE = 2097152L; // 0x200000L
diff --git a/media/media/api/public_plus_experimental_current.txt b/media/media/api/public_plus_experimental_current.txt
index 77263b5..a6eaa11 100644
--- a/media/media/api/public_plus_experimental_current.txt
+++ b/media/media/api/public_plus_experimental_current.txt
@@ -446,6 +446,7 @@
field public static final long ACTION_REWIND = 8L; // 0x8L
field public static final long ACTION_SEEK_TO = 256L; // 0x100L
field public static final long ACTION_SET_CAPTIONING_ENABLED = 1048576L; // 0x100000L
+ field public static final long ACTION_SET_PLAYBACK_SPEED = 4194304L; // 0x400000L
field public static final long ACTION_SET_RATING = 128L; // 0x80L
field public static final long ACTION_SET_REPEAT_MODE = 262144L; // 0x40000L
field public static final long ACTION_SET_SHUFFLE_MODE = 2097152L; // 0x200000L
diff --git a/media/media/api/restricted_current.txt b/media/media/api/restricted_current.txt
index b22a5f6..052b25c 100644
--- a/media/media/api/restricted_current.txt
+++ b/media/media/api/restricted_current.txt
@@ -457,6 +457,7 @@
field public static final long ACTION_REWIND = 8L; // 0x8L
field public static final long ACTION_SEEK_TO = 256L; // 0x100L
field public static final long ACTION_SET_CAPTIONING_ENABLED = 1048576L; // 0x100000L
+ field public static final long ACTION_SET_PLAYBACK_SPEED = 4194304L; // 0x400000L
field public static final long ACTION_SET_RATING = 128L; // 0x80L
field public static final long ACTION_SET_REPEAT_MODE = 262144L; // 0x40000L
field public static final long ACTION_SET_SHUFFLE_MODE = 2097152L; // 0x200000L
@@ -502,7 +503,7 @@
field public static final int STATE_STOPPED = 1; // 0x1
}
- @LongDef(flag=true, value={android.support.v4.media.session.PlaybackStateCompat.ACTION_STOP, android.support.v4.media.session.PlaybackStateCompat.ACTION_PAUSE, android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY, android.support.v4.media.session.PlaybackStateCompat.ACTION_REWIND, android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS, android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT, android.support.v4.media.session.PlaybackStateCompat.ACTION_FAST_FORWARD, android.support.v4.media.session.PlaybackStateCompat.ACTION_SET_RATING, android.support.v4.media.session.PlaybackStateCompat.ACTION_SEEK_TO, android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_PAUSE, android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID, android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH, android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM, android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_FROM_URI, android.support.v4.media.session.PlaybackStateCompat.ACTION_PREPARE, android.support.v4.media.session.PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID, android.support.v4.media.session.PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH, android.support.v4.media.session.PlaybackStateCompat.ACTION_PREPARE_FROM_URI, android.support.v4.media.session.PlaybackStateCompat.ACTION_SET_REPEAT_MODE, android.support.v4.media.session.PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE, android.support.v4.media.session.PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface PlaybackStateCompat.Actions {
+ @LongDef(flag=true, value={android.support.v4.media.session.PlaybackStateCompat.ACTION_STOP, android.support.v4.media.session.PlaybackStateCompat.ACTION_PAUSE, android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY, android.support.v4.media.session.PlaybackStateCompat.ACTION_REWIND, android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS, android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT, android.support.v4.media.session.PlaybackStateCompat.ACTION_FAST_FORWARD, android.support.v4.media.session.PlaybackStateCompat.ACTION_SET_RATING, android.support.v4.media.session.PlaybackStateCompat.ACTION_SEEK_TO, android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_PAUSE, android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID, android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH, android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM, android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_FROM_URI, android.support.v4.media.session.PlaybackStateCompat.ACTION_PREPARE, android.support.v4.media.session.PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID, android.support.v4.media.session.PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH, android.support.v4.media.session.PlaybackStateCompat.ACTION_PREPARE_FROM_URI, android.support.v4.media.session.PlaybackStateCompat.ACTION_SET_REPEAT_MODE, android.support.v4.media.session.PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE, android.support.v4.media.session.PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED, android.support.v4.media.session.PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface PlaybackStateCompat.Actions {
}
public static final class PlaybackStateCompat.Builder {
diff --git a/media/media/src/main/java/android/support/v4/media/session/PlaybackStateCompat.java b/media/media/src/main/java/android/support/v4/media/session/PlaybackStateCompat.java
index 40d565a..d51a003 100644
--- a/media/media/src/main/java/android/support/v4/media/session/PlaybackStateCompat.java
+++ b/media/media/src/main/java/android/support/v4/media/session/PlaybackStateCompat.java
@@ -55,7 +55,8 @@
ACTION_SEEK_TO, ACTION_PLAY_PAUSE, ACTION_PLAY_FROM_MEDIA_ID, ACTION_PLAY_FROM_SEARCH,
ACTION_SKIP_TO_QUEUE_ITEM, ACTION_PLAY_FROM_URI, ACTION_PREPARE,
ACTION_PREPARE_FROM_MEDIA_ID, ACTION_PREPARE_FROM_SEARCH, ACTION_PREPARE_FROM_URI,
- ACTION_SET_REPEAT_MODE, ACTION_SET_SHUFFLE_MODE, ACTION_SET_CAPTIONING_ENABLED})
+ ACTION_SET_REPEAT_MODE, ACTION_SET_SHUFFLE_MODE, ACTION_SET_CAPTIONING_ENABLED,
+ ACTION_SET_PLAYBACK_SPEED})
@Retention(RetentionPolicy.SOURCE)
public @interface Actions {}
@@ -225,6 +226,13 @@
public static final long ACTION_SET_SHUFFLE_MODE = 1 << 21;
/**
+ * Indicates this session supports the set playback speed command.
+ *
+ * @see Builder#setActions(long)
+ */
+ public static final long ACTION_SET_PLAYBACK_SPEED = 1 << 22;
+
+ /**
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX) // used by media2-session
@@ -717,6 +725,7 @@
* <li> {@link PlaybackStateCompat#ACTION_SET_REPEAT_MODE}</li>
* <li> {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE}</li>
* <li> {@link PlaybackStateCompat#ACTION_SET_CAPTIONING_ENABLED}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_SET_PLAYBACK_SPEED}</li>
* </ul>
*/
@Actions
@@ -1255,6 +1264,7 @@
* <li> {@link PlaybackStateCompat#ACTION_SET_REPEAT_MODE}</li>
* <li> {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE}</li>
* <li> {@link PlaybackStateCompat#ACTION_SET_CAPTIONING_ENABLED}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_SET_PLAYBACK_SPEED}</li>
* </ul>
*
* @return this
diff --git a/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/PlaybackStateCompatTest.java b/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/PlaybackStateCompatTest.java
index 6cb6393..353cadc 100644
--- a/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/PlaybackStateCompatTest.java
+++ b/media/version-compat-tests/current/client/src/androidTest/java/android/support/mediacompat/client/PlaybackStateCompatTest.java
@@ -284,6 +284,42 @@
parcel.recycle();
}
+ /**
+ * Tests that each ACTION_* constant does not overlap.
+ */
+ @Test
+ @SmallTest
+ public void testActionConstantDoesNotOverlap() {
+ long[] actionConstants = new long[] {
+ PlaybackStateCompat.ACTION_STOP, PlaybackStateCompat.ACTION_PAUSE,
+ PlaybackStateCompat.ACTION_PLAY, PlaybackStateCompat.ACTION_REWIND,
+ PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS,
+ PlaybackStateCompat.ACTION_SKIP_TO_NEXT,
+ PlaybackStateCompat.ACTION_FAST_FORWARD,
+ PlaybackStateCompat.ACTION_SET_RATING,
+ PlaybackStateCompat.ACTION_SEEK_TO,
+ PlaybackStateCompat.ACTION_PLAY_PAUSE,
+ PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID,
+ PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH,
+ PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM,
+ PlaybackStateCompat.ACTION_PLAY_FROM_URI,
+ PlaybackStateCompat.ACTION_PREPARE,
+ PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID,
+ PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH,
+ PlaybackStateCompat.ACTION_PREPARE_FROM_URI,
+ PlaybackStateCompat.ACTION_SET_REPEAT_MODE,
+ PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE,
+ PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED,
+ PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED};
+
+ // Check that the values are not overlapped.
+ for (int i = 0; i < actionConstants.length; i++) {
+ for (int j = i + 1; j < actionConstants.length; j++) {
+ assertEquals(0, actionConstants[i] & actionConstants[j]);
+ }
+ }
+ }
+
private void assertCustomActionEquals(PlaybackStateCompat.CustomAction action1,
PlaybackStateCompat.CustomAction action2) {
assertEquals(action1.getAction(), action2.getAction());
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaNotificationHandler.java b/media2/session/src/main/java/androidx/media2/session/MediaNotificationHandler.java
index de2971e..07df689 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaNotificationHandler.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaNotificationHandler.java
@@ -122,6 +122,34 @@
mServiceInstance.startForeground(id, notification);
}
+ /**
+ * Updates the notification when needed.
+ * This will be called when the current media item is changed.
+ */
+ @Override
+ public void onNotificationUpdateNeeded(MediaSession session) {
+ MediaSessionService.MediaNotification mediaNotification =
+ mServiceInstance.onUpdateNotification(session);
+ if (mediaNotification == null) {
+ // The service implementation doesn't want to use the automatic start/stopForeground
+ // feature.
+ return;
+ }
+
+ int id = mediaNotification.getNotificationId();
+ Notification notification = mediaNotification.getNotification();
+
+ if (Build.VERSION.SDK_INT >= 21) {
+ // Call Notification.MediaStyle#setMediaSession() indirectly.
+ android.media.session.MediaSession.Token fwkToken =
+ (android.media.session.MediaSession.Token)
+ session.getSessionCompat().getSessionToken().getToken();
+ notification.extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, fwkToken);
+ }
+
+ mNotificationManager.notify(id, notification);
+ }
+
@Override
public void onSessionClosed(MediaSession session) {
mServiceInstance.removeSession(session);
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaSession.java b/media2/session/src/main/java/androidx/media2/session/MediaSession.java
index f6447861..5ba8b5d 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaSession.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaSession.java
@@ -787,6 +787,12 @@
}
}
+ final void onCurrentMediaItemChanged(MediaSession session) {
+ if (mForegroundServiceEventCallback != null) {
+ mForegroundServiceEventCallback.onNotificationUpdateNeeded(session);
+ }
+ }
+
final void onSessionClosed(MediaSession session) {
if (mForegroundServiceEventCallback != null) {
mForegroundServiceEventCallback.onSessionClosed(session);
@@ -799,6 +805,7 @@
abstract static class ForegroundServiceEventCallback {
public void onPlayerStateChanged(MediaSession session, @PlayerState int state) {}
+ public void onNotificationUpdateNeeded(MediaSession session) {}
public void onSessionClosed(MediaSession session) {}
}
}
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaSessionImplBase.java b/media2/session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
index d1db6dd..51caf49 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaSessionImplBase.java
@@ -1389,6 +1389,7 @@
item.addOnMetadataChangedListener(session.mCallbackExecutor, this);
}
mMediaItem = item;
+ session.getCallback().onCurrentMediaItemChanged(session.getInstance());
boolean notifyingPended = false;
if (item != null) {
diff --git a/media2/session/src/main/java/androidx/media2/session/MediaSessionService.java b/media2/session/src/main/java/androidx/media2/session/MediaSessionService.java
index cfed886..d8a459e 100644
--- a/media2/session/src/main/java/androidx/media2/session/MediaSessionService.java
+++ b/media2/session/src/main/java/androidx/media2/session/MediaSessionService.java
@@ -222,7 +222,7 @@
* notification UI.
* <p>
* This would be called on {@link MediaSession}'s callback executor when player state is
- * changed.
+ * changed, or when the current media item of the session is changed.
* <p>
* With the notification returned here, the service becomes foreground service when the playback
* is started. Apps targeting API {@link android.os.Build.VERSION_CODES#P} or later must request
diff --git a/media2/session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceNotificationTest.java b/media2/session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceNotificationTest.java
index e0fb863..b9520b8 100644
--- a/media2/session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceNotificationTest.java
+++ b/media2/session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceNotificationTest.java
@@ -101,11 +101,10 @@
new SessionToken(mContext, MOCK_MEDIA2_SESSION_SERVICE), true, null);
// Set current media item.
- final String mediaId = "testMediaId";
Bitmap albumArt = BitmapFactory.decodeResource(mContext.getResources(),
androidx.media2.test.service.R.drawable.big_buck_bunny);
MediaMetadata metadata = new MediaMetadata.Builder()
- .putText(METADATA_KEY_MEDIA_ID, mediaId)
+ .putText(METADATA_KEY_MEDIA_ID, "testMediaId")
.putText(METADATA_KEY_DISPLAY_TITLE, "Test Song Name")
.putText(METADATA_KEY_ARTIST, "Test Artist Name")
.putBitmap(METADATA_KEY_ALBUM_ART, albumArt)
@@ -122,4 +121,63 @@
mPlayer.notifyPlayerStateChanged(SessionPlayer.PLAYER_STATE_PLAYING);
Thread.sleep(NOTIFICATION_SHOW_TIME_MS);
}
+
+ @Test
+ @Ignore("Comment out this line and manually run the test.")
+ public void notificationUpdatedWhenCurrentMediaItemChanged() throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
+ @Override
+ public SessionCommandGroup onConnect(@NonNull MediaSession session,
+ @NonNull ControllerInfo controller) {
+ if (CLIENT_PACKAGE_NAME.equals(controller.getPackageName())) {
+ mSession = session;
+ // Change the player and playlist agent with ours.
+ session.updatePlayer(mPlayer);
+ latch.countDown();
+ }
+ return super.onConnect(session, controller);
+ }
+ };
+ TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
+
+ // Create a controller to start the service.
+ RemoteMediaController controller = createRemoteController(
+ new SessionToken(mContext, MOCK_MEDIA2_SESSION_SERVICE), true, null);
+
+ // Set current media item.
+ Bitmap albumArt = BitmapFactory.decodeResource(mContext.getResources(),
+ androidx.media2.test.service.R.drawable.big_buck_bunny);
+ MediaMetadata metadata = new MediaMetadata.Builder()
+ .putText(METADATA_KEY_MEDIA_ID, "testMediaId")
+ .putText(METADATA_KEY_DISPLAY_TITLE, "Test Song Name")
+ .putText(METADATA_KEY_ARTIST, "Test Artist Name")
+ .putBitmap(METADATA_KEY_ALBUM_ART, albumArt)
+ .putLong(METADATA_KEY_BROWSABLE, BROWSABLE_TYPE_NONE)
+ .putLong(METADATA_KEY_PLAYABLE, 1)
+ .build();
+ mPlayer.mCurrentMediaItem = new MediaItem.Builder()
+ .setMetadata(metadata)
+ .build();
+
+ mPlayer.notifyPlayerStateChanged(SessionPlayer.PLAYER_STATE_PLAYING);
+ // At this point, the notification should be shown.
+ Thread.sleep(NOTIFICATION_SHOW_TIME_MS);
+
+ // Set a new media item. (current media item is changed)
+ MediaMetadata newMetadata = new MediaMetadata.Builder()
+ .putText(METADATA_KEY_MEDIA_ID, "New media ID")
+ .putText(METADATA_KEY_DISPLAY_TITLE, "New Song Name")
+ .putText(METADATA_KEY_ARTIST, "New Artist Name")
+ .putLong(METADATA_KEY_BROWSABLE, BROWSABLE_TYPE_NONE)
+ .putLong(METADATA_KEY_PLAYABLE, 1)
+ .build();
+
+ MediaItem newItem = new MediaItem.Builder().setMetadata(newMetadata).build();
+ mPlayer.mCurrentMediaItem = newItem;
+
+ // Calling this should update the notification with the new metadata.
+ mPlayer.notifyCurrentMediaItemChanged(newItem);
+ Thread.sleep(NOTIFICATION_SHOW_TIME_MS);
+ }
}
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavByDeepLinkDemo.kt b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavByDeepLinkDemo.kt
index 6a64d84..e0469ed 100644
--- a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavByDeepLinkDemo.kt
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavByDeepLinkDemo.kt
@@ -23,7 +23,7 @@
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants
+import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Divider
import androidx.compose.material.Text
import androidx.compose.material.TextField
@@ -74,7 +74,7 @@
Divider(color = Color.Black)
Button(
onClick = { navController.navigate(Uri.parse(uri + state.value)) },
- colors = ButtonConstants.defaultButtonColors(backgroundColor = Color.LightGray),
+ colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray),
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Navigate By DeepLink")
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavPopUpToDemo.kt b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavPopUpToDemo.kt
index fd00b02..a195480a 100644
--- a/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavPopUpToDemo.kt
+++ b/navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/NavPopUpToDemo.kt
@@ -21,7 +21,7 @@
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants
+import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -53,7 +53,7 @@
if (number < 5) {
Button(
onClick = { navController.navigate("$next") },
- colors = ButtonConstants.defaultButtonColors(backgroundColor = Color.LightGray),
+ colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray),
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Navigate to Screen $next")
@@ -63,7 +63,7 @@
if (navController.previousBackStackEntry != null) {
Button(
onClick = { navController.navigate("1") { popUpTo("1") { inclusive = true } } },
- colors = ButtonConstants.defaultButtonColors(backgroundColor = Color.LightGray),
+ colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray),
modifier = Modifier.fillMaxWidth()
) {
Text(text = "PopUpTo Screen 1")
diff --git a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/NavigationSamples.kt b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/NavigationSamples.kt
index 4da4784..89c6817 100644
--- a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/NavigationSamples.kt
+++ b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/NavigationSamples.kt
@@ -25,7 +25,7 @@
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
-import androidx.compose.material.ButtonConstants
+import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Divider
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@@ -133,7 +133,7 @@
) {
Button(
onClick = listener,
- colors = ButtonConstants.defaultButtonColors(backgroundColor = LightGray),
+ colors = ButtonDefaults.buttonColors(backgroundColor = LightGray),
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Navigate to $text")
@@ -145,7 +145,7 @@
if (navController.previousBackStackEntry != null) {
Button(
onClick = { navController.popBackStack() },
- colors = ButtonConstants.defaultButtonColors(backgroundColor = LightGray),
+ colors = ButtonDefaults.buttonColors(backgroundColor = LightGray),
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Go to Previous screen")
diff --git a/paging/common/api/current.txt b/paging/common/api/current.txt
index d72a0d9a..c0867a3 100644
--- a/paging/common/api/current.txt
+++ b/paging/common/api/current.txt
@@ -9,10 +9,7 @@
}
public final class CombinedLoadStates {
- ctor public CombinedLoadStates(androidx.paging.LoadStates source, androidx.paging.LoadStates? mediator);
- method public androidx.paging.LoadStates component1();
- method public androidx.paging.LoadStates? component2();
- method public androidx.paging.CombinedLoadStates copy(androidx.paging.LoadStates source, androidx.paging.LoadStates? mediator);
+ ctor public CombinedLoadStates(androidx.paging.LoadState refresh, androidx.paging.LoadState prepend, androidx.paging.LoadState append, androidx.paging.LoadStates source, androidx.paging.LoadStates? mediator);
method public androidx.paging.LoadState getAppend();
method public androidx.paging.LoadStates? getMediator();
method public androidx.paging.LoadState getPrepend();
@@ -265,7 +262,7 @@
}
public final class Pager<Key, Value> {
- ctor public Pager(androidx.paging.PagingConfig config, Key? initialKey, androidx.paging.RemoteMediator<Key,Value>? remoteMediator, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
+ ctor @androidx.paging.ExperimentalPagingApi public Pager(androidx.paging.PagingConfig config, Key? initialKey, androidx.paging.RemoteMediator<Key,Value>? remoteMediator, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
ctor public Pager(androidx.paging.PagingConfig config, Key? initialKey, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
ctor public Pager(androidx.paging.PagingConfig config, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
method public kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>> getFlow();
@@ -318,8 +315,8 @@
method public final boolean getInvalid();
method public boolean getJumpingSupported();
method public boolean getKeyReuseSupported();
- method @androidx.paging.ExperimentalPagingApi public Key? getRefreshKey(androidx.paging.PagingState<Key,Value> state);
- method public void invalidate();
+ method public Key? getRefreshKey(androidx.paging.PagingState<Key,Value> state);
+ method public final void invalidate();
method public abstract suspend Object? load(androidx.paging.PagingSource.LoadParams<Key> params, kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>> p);
method public final void registerInvalidatedCallback(kotlin.jvm.functions.Function0<kotlin.Unit> onInvalidatedCallback);
method public final void unregisterInvalidatedCallback(kotlin.jvm.functions.Function0<kotlin.Unit> onInvalidatedCallback);
diff --git a/paging/common/api/public_plus_experimental_current.txt b/paging/common/api/public_plus_experimental_current.txt
index f286ae0..9641951 100644
--- a/paging/common/api/public_plus_experimental_current.txt
+++ b/paging/common/api/public_plus_experimental_current.txt
@@ -9,11 +9,7 @@
}
public final class CombinedLoadStates {
- ctor public CombinedLoadStates(androidx.paging.LoadStates source, androidx.paging.LoadStates? mediator);
- method public androidx.paging.LoadStates component1();
- method public androidx.paging.LoadStates? component2();
- method public androidx.paging.CombinedLoadStates copy(androidx.paging.LoadStates source, androidx.paging.LoadStates? mediator);
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public inline void forEach(kotlin.jvm.functions.Function3<? super androidx.paging.LoadType,? super java.lang.Boolean,? super androidx.paging.LoadState,kotlin.Unit> op);
+ ctor public CombinedLoadStates(androidx.paging.LoadState refresh, androidx.paging.LoadState prepend, androidx.paging.LoadState append, androidx.paging.LoadStates source, androidx.paging.LoadStates? mediator);
method public androidx.paging.LoadState getAppend();
method public androidx.paging.LoadStates? getMediator();
method public androidx.paging.LoadState getPrepend();
@@ -267,7 +263,7 @@
}
public final class Pager<Key, Value> {
- ctor public Pager(androidx.paging.PagingConfig config, Key? initialKey, androidx.paging.RemoteMediator<Key,Value>? remoteMediator, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
+ ctor @androidx.paging.ExperimentalPagingApi public Pager(androidx.paging.PagingConfig config, Key? initialKey, androidx.paging.RemoteMediator<Key,Value>? remoteMediator, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
ctor public Pager(androidx.paging.PagingConfig config, Key? initialKey, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
ctor public Pager(androidx.paging.PagingConfig config, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
method public kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>> getFlow();
@@ -320,8 +316,8 @@
method public final boolean getInvalid();
method public boolean getJumpingSupported();
method public boolean getKeyReuseSupported();
- method @androidx.paging.ExperimentalPagingApi public Key? getRefreshKey(androidx.paging.PagingState<Key,Value> state);
- method public void invalidate();
+ method public Key? getRefreshKey(androidx.paging.PagingState<Key,Value> state);
+ method public final void invalidate();
method public abstract suspend Object? load(androidx.paging.PagingSource.LoadParams<Key> params, kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>> p);
method public final void registerInvalidatedCallback(kotlin.jvm.functions.Function0<kotlin.Unit> onInvalidatedCallback);
method public final void unregisterInvalidatedCallback(kotlin.jvm.functions.Function0<kotlin.Unit> onInvalidatedCallback);
diff --git a/paging/common/api/restricted_current.txt b/paging/common/api/restricted_current.txt
index d72a0d9a..c0867a3 100644
--- a/paging/common/api/restricted_current.txt
+++ b/paging/common/api/restricted_current.txt
@@ -9,10 +9,7 @@
}
public final class CombinedLoadStates {
- ctor public CombinedLoadStates(androidx.paging.LoadStates source, androidx.paging.LoadStates? mediator);
- method public androidx.paging.LoadStates component1();
- method public androidx.paging.LoadStates? component2();
- method public androidx.paging.CombinedLoadStates copy(androidx.paging.LoadStates source, androidx.paging.LoadStates? mediator);
+ ctor public CombinedLoadStates(androidx.paging.LoadState refresh, androidx.paging.LoadState prepend, androidx.paging.LoadState append, androidx.paging.LoadStates source, androidx.paging.LoadStates? mediator);
method public androidx.paging.LoadState getAppend();
method public androidx.paging.LoadStates? getMediator();
method public androidx.paging.LoadState getPrepend();
@@ -265,7 +262,7 @@
}
public final class Pager<Key, Value> {
- ctor public Pager(androidx.paging.PagingConfig config, Key? initialKey, androidx.paging.RemoteMediator<Key,Value>? remoteMediator, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
+ ctor @androidx.paging.ExperimentalPagingApi public Pager(androidx.paging.PagingConfig config, Key? initialKey, androidx.paging.RemoteMediator<Key,Value>? remoteMediator, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
ctor public Pager(androidx.paging.PagingConfig config, Key? initialKey, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
ctor public Pager(androidx.paging.PagingConfig config, kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
method public kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>> getFlow();
@@ -318,8 +315,8 @@
method public final boolean getInvalid();
method public boolean getJumpingSupported();
method public boolean getKeyReuseSupported();
- method @androidx.paging.ExperimentalPagingApi public Key? getRefreshKey(androidx.paging.PagingState<Key,Value> state);
- method public void invalidate();
+ method public Key? getRefreshKey(androidx.paging.PagingState<Key,Value> state);
+ method public final void invalidate();
method public abstract suspend Object? load(androidx.paging.PagingSource.LoadParams<Key> params, kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>> p);
method public final void registerInvalidatedCallback(kotlin.jvm.functions.Function0<kotlin.Unit> onInvalidatedCallback);
method public final void unregisterInvalidatedCallback(kotlin.jvm.functions.Function0<kotlin.Unit> onInvalidatedCallback);
diff --git a/paging/common/src/main/kotlin/androidx/paging/CachedPageEventFlow.kt b/paging/common/src/main/kotlin/androidx/paging/CachedPageEventFlow.kt
index 2b4bee4..de3a633 100644
--- a/paging/common/src/main/kotlin/androidx/paging/CachedPageEventFlow.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/CachedPageEventFlow.kt
@@ -42,7 +42,6 @@
* An intermediate flow producer that flattens previous page events and gives any new downstream
* just those events instead of the full history.
*/
-@OptIn(ExperimentalCoroutinesApi::class)
internal class CachedPageEventFlow<T : Any>(
src: Flow<PageEvent<T>>,
scope: CoroutineScope
@@ -81,6 +80,7 @@
multicastedSrc.close()
}
+ @OptIn(ExperimentalCoroutinesApi::class)
val downstreamFlow = channelFlow {
// get a new snapshot. this will immediately hook us to the upstream channel
val snapshot = pageController.createTemporaryDownstream()
@@ -141,7 +141,6 @@
*/
private val historyChannel: Channel<IndexedValue<PageEvent<T>>> = Channel(Channel.UNLIMITED)
- @OptIn(ExperimentalCoroutinesApi::class)
fun consumeHistory() = historyChannel.consumeAsFlow()
/**
diff --git a/paging/common/src/main/kotlin/androidx/paging/CachedPagingData.kt b/paging/common/src/main/kotlin/androidx/paging/CachedPagingData.kt
index 828d8a0..630c588 100644
--- a/paging/common/src/main/kotlin/androidx/paging/CachedPagingData.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/CachedPagingData.kt
@@ -30,7 +30,6 @@
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.scan
-@OptIn(ExperimentalCoroutinesApi::class)
private class MulticastedPagingData<T : Any>(
val scope: CoroutineScope,
val parent: PagingData<T>,
diff --git a/paging/common/src/main/kotlin/androidx/paging/CombinedLoadStates.kt b/paging/common/src/main/kotlin/androidx/paging/CombinedLoadStates.kt
index 386b4ba..19efffd 100644
--- a/paging/common/src/main/kotlin/androidx/paging/CombinedLoadStates.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/CombinedLoadStates.kt
@@ -16,12 +16,43 @@
package androidx.paging
-import androidx.annotation.RestrictTo
-
/**
* Collection of pagination [LoadState]s for both a [PagingSource], and [RemoteMediator].
*/
-data class CombinedLoadStates(
+class CombinedLoadStates(
+ /**
+ * Convenience for combined behavior of [REFRESH][LoadType.REFRESH] [LoadState], which
+ * generally defers to [mediator] if it exists, but if previously was [LoadState.Loading],
+ * awaits for both [source] and [mediator] to become [LoadState.NotLoading] to ensure the
+ * remote load was applied.
+ *
+ * For use cases that require reacting to [LoadState] of [source] and [mediator]
+ * specifically, e.g., showing cached data when network loads via [mediator] fail,
+ * [LoadStates] exposed via [source] and [mediator] should be used directly.
+ */
+ val refresh: LoadState,
+ /**
+ * Convenience for combined behavior of [PREPEND][LoadType.REFRESH] [LoadState], which
+ * generally defers to [mediator] if it exists, but if previously was [LoadState.Loading],
+ * awaits for both [source] and [mediator] to become [LoadState.NotLoading] to ensure the
+ * remote load was applied.
+ *
+ * For use cases that require reacting to [LoadState] of [source] and [mediator]
+ * specifically, e.g., showing cached data when network loads via [mediator] fail,
+ * [LoadStates] exposed via [source] and [mediator] should be used directly.
+ */
+ val prepend: LoadState,
+ /**
+ * Convenience for combined behavior of [APPEND][LoadType.REFRESH] [LoadState], which
+ * generally defers to [mediator] if it exists, but if previously was [LoadState.Loading],
+ * awaits for both [source] and [mediator] to become [LoadState.NotLoading] to ensure the
+ * remote load was applied.
+ *
+ * For use cases that require reacting to [LoadState] of [source] and [mediator]
+ * specifically, e.g., showing cached data when network loads via [mediator] fail,
+ * [LoadStates] exposed via [source] and [mediator] should be used directly.
+ */
+ val append: LoadState,
/**
* [LoadStates] corresponding to loads from a [PagingSource].
*/
@@ -31,40 +62,39 @@
* [LoadStates] corresponding to loads from a [RemoteMediator], or `null` if [RemoteMediator]
* not present.
*/
- val mediator: LoadStates? = null
+ val mediator: LoadStates? = null,
) {
- /**
- * Convenience for accessing [REFRESH][LoadType.REFRESH] [LoadState], which always defers to
- * [LoadState] of [mediator] if it exists, otherwise equivalent to [LoadState] of [source].
- *
- * For use cases that require reacting to [LoadState] of [source] and [mediator]
- * specifically, e.g., showing cached data when network loads via [mediator] fail,
- * [LoadStates] exposed via [source] and [mediator] should be used directly.
- */
- val refresh: LoadState = (mediator ?: source).refresh
- /**
- * Convenience for accessing [PREPEND][LoadType.PREPEND] [LoadState], which always defers to
- * [LoadState] of [mediator] if it exists, otherwise equivalent to [LoadState] of [source].
- *
- * For use cases that require reacting to [LoadState] of [source] and [mediator]
- * specifically, e.g., showing cached data when network loads via [mediator] fail,
- * [LoadStates] exposed via [source] and [mediator] should be used directly.
- */
- val prepend: LoadState = (mediator ?: source).prepend
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
- /**
- * Convenience for accessing [APPEND][LoadType.APPEND] [LoadState], which always defers to
- * [LoadState] of [mediator] if it exists, otherwise equivalent to [LoadState] of [source].
- *
- * For use cases that require reacting to [LoadState] of [source] and [mediator]
- * specifically, e.g., showing cached data when network loads via [mediator] fail,
- * [LoadStates] exposed via [source] and [mediator] should be used directly.
- */
- val append: LoadState = (mediator ?: source).append
+ other as CombinedLoadStates
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- inline fun forEach(op: (LoadType, Boolean, LoadState) -> Unit) {
+ if (refresh != other.refresh) return false
+ if (prepend != other.prepend) return false
+ if (append != other.append) return false
+ if (source != other.source) return false
+ if (mediator != other.mediator) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = refresh.hashCode()
+ result = 31 * result + prepend.hashCode()
+ result = 31 * result + append.hashCode()
+ result = 31 * result + source.hashCode()
+ result = 31 * result + (mediator?.hashCode() ?: 0)
+ return result
+ }
+
+ override fun toString(): String {
+ return "CombinedLoadStates(refresh=$refresh, prepend=$prepend, append=$append, " +
+ "source=$source, mediator=$mediator)"
+ }
+
+ internal fun forEach(op: (LoadType, Boolean, LoadState) -> Unit) {
source.forEach { type, state ->
op(type, false, state)
}
@@ -75,11 +105,10 @@
internal companion object {
val IDLE_SOURCE = CombinedLoadStates(
+ refresh = LoadState.NotLoading.Incomplete,
+ prepend = LoadState.NotLoading.Incomplete,
+ append = LoadState.NotLoading.Incomplete,
source = LoadStates.IDLE
)
- val IDLE_MEDIATOR = CombinedLoadStates(
- source = LoadStates.IDLE,
- mediator = LoadStates.IDLE
- )
}
}
diff --git a/paging/common/src/main/kotlin/androidx/paging/ConflatedEventBus.kt b/paging/common/src/main/kotlin/androidx/paging/ConflatedEventBus.kt
index d896993..3679a19 100644
--- a/paging/common/src/main/kotlin/androidx/paging/ConflatedEventBus.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/ConflatedEventBus.kt
@@ -16,7 +16,6 @@
package androidx.paging
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.mapNotNull
@@ -25,7 +24,6 @@
* * It allows not setting an initial value
* * Sending duplicate values is allowed
*/
-@OptIn(ExperimentalCoroutinesApi::class)
internal class ConflatedEventBus<T : Any>(initialValue: T? = null) {
private val state = MutableStateFlow(Pair(Integer.MIN_VALUE, initialValue))
diff --git a/paging/common/src/main/kotlin/androidx/paging/ContiguousPagedList.kt b/paging/common/src/main/kotlin/androidx/paging/ContiguousPagedList.kt
index 055f941..e7d9382 100644
--- a/paging/common/src/main/kotlin/androidx/paging/ContiguousPagedList.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/ContiguousPagedList.kt
@@ -97,7 +97,6 @@
@Suppress("UNCHECKED_CAST")
override val lastKey: K?
get() {
- @OptIn(ExperimentalPagingApi::class)
return (storage.getRefreshKeyInfo(config) as PagingState<K, V>?)
?.let { pagingSource.getRefreshKey(it) }
?: initialLastKey
diff --git a/paging/common/src/main/kotlin/androidx/paging/DataSource.kt b/paging/common/src/main/kotlin/androidx/paging/DataSource.kt
index 26e0270..cbceeae 100644
--- a/paging/common/src/main/kotlin/androidx/paging/DataSource.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/DataSource.kt
@@ -233,9 +233,12 @@
@JvmOverloads
fun asPagingSourceFactory(
fetchDispatcher: CoroutineDispatcher = Dispatchers.IO
- ): () -> PagingSource<Key, Value> = {
- LegacyPagingSource(fetchDispatcher) { create() }
- }
+ ): () -> PagingSource<Key, Value> = SuspendingPagingSourceFactory(
+ delegate = {
+ LegacyPagingSource(fetchDispatcher, create())
+ },
+ dispatcher = fetchDispatcher
+ )
}
/**
diff --git a/paging/common/src/main/kotlin/androidx/paging/DirectDispatcher.kt b/paging/common/src/main/kotlin/androidx/paging/DirectDispatcher.kt
deleted file mode 100644
index 5d65446..0000000
--- a/paging/common/src/main/kotlin/androidx/paging/DirectDispatcher.kt
+++ /dev/null
@@ -1,29 +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.paging
-
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlin.coroutines.CoroutineContext
-
-/**
- * [CoroutineDispatcher] which immediately runs new jobs on the current thread.
- */
-internal object DirectDispatcher : CoroutineDispatcher() {
- override fun dispatch(context: CoroutineContext, block: Runnable) {
- block.run()
- }
-}
\ No newline at end of file
diff --git a/paging/common/src/main/kotlin/androidx/paging/InitialPagedList.kt b/paging/common/src/main/kotlin/androidx/paging/InitialPagedList.kt
index 5d24516..7014826 100644
--- a/paging/common/src/main/kotlin/androidx/paging/InitialPagedList.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/InitialPagedList.kt
@@ -17,6 +17,7 @@
package androidx.paging
import androidx.annotation.RestrictTo
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
/**
@@ -31,15 +32,17 @@
class InitialPagedList<K : Any, V : Any>(
pagingSource: PagingSource<K, V>,
coroutineScope: CoroutineScope,
+ notifyDispatcher: CoroutineDispatcher,
+ backgroundDispatcher: CoroutineDispatcher,
config: Config,
initialLastKey: K?
) : ContiguousPagedList<K, V>(
- pagingSource,
- coroutineScope,
- DirectDispatcher,
- DirectDispatcher,
- null,
- config,
- PagingSource.LoadResult.Page.empty(),
- initialLastKey
+ pagingSource = pagingSource,
+ coroutineScope = coroutineScope,
+ notifyDispatcher = notifyDispatcher,
+ backgroundDispatcher = backgroundDispatcher,
+ boundaryCallback = null,
+ config = config,
+ initialPage = PagingSource.LoadResult.Page.empty(),
+ initialLastKey = initialLastKey
)
diff --git a/paging/common/src/main/kotlin/androidx/paging/LegacyPagingSource.kt b/paging/common/src/main/kotlin/androidx/paging/LegacyPagingSource.kt
index 43283af..c5d7845 100644
--- a/paging/common/src/main/kotlin/androidx/paging/LegacyPagingSource.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/LegacyPagingSource.kt
@@ -30,24 +30,25 @@
* A wrapper around [DataSource] which adapts it to the [PagingSource] API.
*/
internal class LegacyPagingSource<Key : Any, Value : Any>(
- private val fetchDispatcher: CoroutineDispatcher = DirectDispatcher,
- internal val dataSourceFactory: () -> DataSource<Key, Value>
+ private val fetchDispatcher: CoroutineDispatcher,
+ internal val dataSource: DataSource<Key, Value>
) : PagingSource<Key, Value>() {
private var pageSize: Int = PAGE_SIZE_NOT_SET
- // Lazily initialize because it must be created on fetchDispatcher, but PagingSourceFactory
- // passed to Pager is a non-suspending method.
- internal val dataSource by lazy {
- dataSourceFactory().also { dataSource ->
- dataSource.addInvalidatedCallback(::invalidate)
- // LegacyPagingSource registers invalidate callback after DataSource is created, so we
- // need to check for race condition here. If DataSource is already invalid, simply
- // propagate invalidation manually.
- if (dataSource.isInvalid && !invalid) {
- dataSource.removeInvalidatedCallback(::invalidate)
- // Note: Calling this.invalidate directly will re-evaluate dataSource's by lazy
- // init block, since we haven't returned a value for dataSource yet.
- super.invalidate()
- }
+
+ init {
+ dataSource.addInvalidatedCallback(::invalidate)
+ // technically, there is a possibly race where data source might call back our invalidate.
+ // in practice, it is fine because all fields are initialized at this point.
+ registerInvalidatedCallback {
+ dataSource.removeInvalidatedCallback(::invalidate)
+ dataSource.invalidate()
+ }
+
+ // LegacyPagingSource registers invalidate callback after DataSource is created, so we
+ // need to check for race condition here. If DataSource is already invalid, simply
+ // propagate invalidation manually.
+ if (!invalid && dataSource.isInvalid) {
+ invalidate()
}
}
@@ -118,12 +119,6 @@
}
}
- override fun invalidate() {
- super.invalidate()
- dataSource.invalidate()
- }
-
- @OptIn(ExperimentalPagingApi::class)
@Suppress("UNCHECKED_CAST")
override fun getRefreshKey(state: PagingState<Key, Value>): Key? {
return when (dataSource.type) {
diff --git a/paging/common/src/main/kotlin/androidx/paging/MutableLoadStateCollection.kt b/paging/common/src/main/kotlin/androidx/paging/MutableLoadStateCollection.kt
index baaf577..31ed7eb 100644
--- a/paging/common/src/main/kotlin/androidx/paging/MutableLoadStateCollection.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/MutableLoadStateCollection.kt
@@ -16,27 +16,45 @@
package androidx.paging
-import androidx.annotation.RestrictTo
+import androidx.paging.LoadState.Error
+import androidx.paging.LoadState.Loading
+import androidx.paging.LoadState.NotLoading
/**
- * TODO: Remove this once [PageEvent.LoadStateUpdate] contained [CombinedLoadStates].
- *
- * @hide
+ * Helper to construct [CombinedLoadStates] that accounts for previous state to set the convenience
+ * properties correctly.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class MutableLoadStateCollection {
+internal class MutableLoadStateCollection {
+ private var refresh: LoadState = NotLoading.Incomplete
+ private var prepend: LoadState = NotLoading.Incomplete
+ private var append: LoadState = NotLoading.Incomplete
private var source: LoadStates = LoadStates.IDLE
private var mediator: LoadStates? = null
- fun snapshot() = CombinedLoadStates(source, mediator)
+ fun snapshot() = CombinedLoadStates(
+ refresh = refresh,
+ prepend = prepend,
+ append = append,
+ source = source,
+ mediator = mediator,
+ )
fun set(combinedLoadStates: CombinedLoadStates) {
+ refresh = combinedLoadStates.refresh
+ prepend = combinedLoadStates.prepend
+ append = combinedLoadStates.append
source = combinedLoadStates.source
mediator = combinedLoadStates.mediator
}
+ fun set(sourceLoadStates: LoadStates, remoteLoadStates: LoadStates?) {
+ source = sourceLoadStates
+ mediator = remoteLoadStates
+ updateHelperStates()
+ }
+
fun set(type: LoadType, remote: Boolean, state: LoadState): Boolean {
- return if (remote) {
+ val didChange = if (remote) {
val lastMediator = mediator
mediator = (mediator ?: LoadStates.IDLE).modifyState(type, state)
mediator != lastMediator
@@ -45,12 +63,61 @@
source = source.modifyState(type, state)
source != lastSource
}
+
+ updateHelperStates()
+ return didChange
}
fun get(type: LoadType, remote: Boolean): LoadState? {
return (if (remote) mediator else source)?.get(type)
}
+ private fun updateHelperStates() {
+ refresh = computeHelperState(
+ previousState = refresh,
+ sourceRefreshState = source.refresh,
+ sourceState = source.refresh,
+ remoteState = mediator?.refresh
+ )
+ prepend = computeHelperState(
+ previousState = prepend,
+ sourceRefreshState = source.refresh,
+ sourceState = source.prepend,
+ remoteState = mediator?.prepend
+ )
+ append = computeHelperState(
+ previousState = append,
+ sourceRefreshState = source.refresh,
+ sourceState = source.append,
+ remoteState = mediator?.append
+ )
+ }
+
+ /**
+ * Computes the next value for the convenience helpers in [CombinedLoadStates], which
+ * generally defers to remote state, but waits for both source and remote states to become
+ * [NotLoading] before moving to that state. This provides a reasonable default for the common
+ * use-case where you generally want to wait for both RemoteMediator to return and for the
+ * update to get applied before signaling to UI that a network fetch has "finished".
+ */
+ private fun computeHelperState(
+ previousState: LoadState,
+ sourceRefreshState: LoadState,
+ sourceState: LoadState,
+ remoteState: LoadState?
+ ): LoadState {
+ if (remoteState == null) return sourceState
+
+ return when (previousState) {
+ is Loading -> when {
+ sourceRefreshState is NotLoading && remoteState is NotLoading -> remoteState
+ remoteState is Error -> remoteState
+ else -> previousState
+ }
+ else -> remoteState
+ }
+ }
+
internal inline fun forEach(op: (LoadType, Boolean, LoadState) -> Unit) {
source.forEach { type, state ->
op(type, false, state)
@@ -59,4 +126,9 @@
op(type, true, state)
}
}
+
+ internal fun terminates(loadType: LoadType): Boolean {
+ return get(loadType, false)!!.endOfPaginationReached &&
+ get(loadType, true)?.endOfPaginationReached != false
+ }
}
diff --git a/paging/common/src/main/kotlin/androidx/paging/PageEvent.kt b/paging/common/src/main/kotlin/androidx/paging/PageEvent.kt
index 827e631..f44eb824 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PageEvent.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PageEvent.kt
@@ -39,11 +39,11 @@
) : PageEvent<T>() {
init {
require(loadType == APPEND || placeholdersBefore >= 0) {
- "Append state defining placeholdersBefore must be > 0, but was" +
+ "Prepend insert defining placeholdersBefore must be > 0, but was" +
" $placeholdersBefore"
}
require(loadType == PREPEND || placeholdersAfter >= 0) {
- "Prepend state defining placeholdersAfter must be > 0, but was" +
+ "Append insert defining placeholdersAfter must be > 0, but was" +
" $placeholdersAfter"
}
require(loadType != REFRESH || pages.isNotEmpty()) {
@@ -150,11 +150,14 @@
placeholdersBefore = 0,
placeholdersAfter = 0,
combinedLoadStates = CombinedLoadStates(
+ refresh = LoadState.NotLoading.Incomplete,
+ prepend = LoadState.NotLoading.Complete,
+ append = LoadState.NotLoading.Complete,
source = LoadStates(
refresh = LoadState.NotLoading.Incomplete,
prepend = LoadState.NotLoading.Complete,
- append = LoadState.NotLoading.Complete
- )
+ append = LoadState.NotLoading.Complete,
+ ),
)
)
}
@@ -212,8 +215,7 @@
* This prevents multiple related RV animations from happening simultaneously
*/
internal fun canDispatchWithoutInsert(loadState: LoadState, fromMediator: Boolean) =
- loadState is LoadState.Loading || loadState is LoadState.Error ||
- (loadState.endOfPaginationReached && fromMediator)
+ loadState is LoadState.Loading || loadState is LoadState.Error || fromMediator
}
}
diff --git a/paging/common/src/main/kotlin/androidx/paging/PageFetcher.kt b/paging/common/src/main/kotlin/androidx/paging/PageFetcher.kt
index 35a9b12..06d6d7a 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PageFetcher.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PageFetcher.kt
@@ -31,9 +31,9 @@
import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.launch
-@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
+@OptIn(FlowPreview::class)
internal class PageFetcher<Key : Any, Value : Any>(
- private val pagingSourceFactory: () -> PagingSource<Key, Value>,
+ private val pagingSourceFactory: suspend () -> PagingSource<Key, Value>,
private val initialKey: Key?,
private val config: PagingConfig,
@OptIn(ExperimentalPagingApi::class)
@@ -53,6 +53,7 @@
// The object built by paging builder can maintain the scope so that on rotation we don't stop
// the paging.
+ @OptIn(ExperimentalCoroutinesApi::class)
val flow: Flow<PagingData<Value>> = channelFlow {
val remoteMediatorAccessor = remoteMediator?.let {
RemoteMediatorAccessor(this, it)
@@ -87,7 +88,6 @@
previousPagingState = previousGeneration.state
}
- @OptIn(ExperimentalPagingApi::class)
val initialKey: Key? = previousPagingState?.let { pagingSource.getRefreshKey(it) }
?: initialKey
@@ -110,11 +110,10 @@
}
.filterNotNull()
.mapLatest { generation ->
- val downstreamFlow = if (remoteMediatorAccessor == null) {
- generation.snapshot.pageEventFlow
- } else {
- generation.snapshot.injectRemoteEvents(remoteMediatorAccessor)
- }
+ val downstreamFlow = generation.snapshot
+ .injectRemoteEvents(remoteMediatorAccessor)
+ // .mapRemoteCompleteAsTrailingInsertForSeparators()
+
PagingData(
flow = downstreamFlow,
receiver = PagerUiReceiver(generation.snapshot, retryEvents)
@@ -124,47 +123,79 @@
}
private fun PageFetcherSnapshot<Key, Value>.injectRemoteEvents(
- accessor: RemoteMediatorAccessor<Key, Value>
- ): Flow<PageEvent<Value>> = channelFlow {
- suspend fun dispatchIfValid(type: LoadType, state: LoadState) {
- // not loading events are sent w/ insert-drop events.
- if (PageEvent.LoadStateUpdate.canDispatchWithoutInsert(state, fromMediator = true)) {
- send(
- PageEvent.LoadStateUpdate<Value>(type, true, state)
- )
- } else {
- // ignore. Some invalidation will happened and we'll send the event there instead
- }
- }
- launch {
- var prev = LoadStates.IDLE
- accessor.state.collect {
- if (prev.refresh != it.refresh) {
- dispatchIfValid(REFRESH, it.refresh)
- }
- if (prev.prepend != it.prepend) {
- dispatchIfValid(PREPEND, it.prepend)
- }
- if (prev.append != it.append) {
- dispatchIfValid(APPEND, it.append)
- }
- prev = it
- }
- }
+ accessor: RemoteMediatorAccessor<Key, Value>?
+ ): Flow<PageEvent<Value>> {
+ if (accessor == null) return pageEventFlow
- this@injectRemoteEvents.pageEventFlow.collect {
- // only insert events have combinedLoadStates.
- if (it is PageEvent.Insert<Value>) {
- send(
- it.copy(
- combinedLoadStates = CombinedLoadStates(
- it.combinedLoadStates.source,
- accessor.state.value
+ @OptIn(ExperimentalCoroutinesApi::class)
+ return channelFlow {
+ val loadStates = MutableLoadStateCollection()
+
+ suspend fun dispatchIfValid(type: LoadType, state: LoadState) {
+ // not loading events are sent w/ insert-drop events.
+ if (PageEvent.LoadStateUpdate.canDispatchWithoutInsert(
+ state,
+ fromMediator = true
+ )
+ ) {
+ send(
+ PageEvent.LoadStateUpdate<Value>(
+ loadType = type,
+ fromMediator = true,
+ loadState = state
)
)
- )
- } else {
- send(it)
+ } else {
+ // Wait for invalidation to set state to NotLoading via Insert to prevent any
+ // potential for flickering.
+ }
+ }
+
+ launch {
+ var prev = LoadStates.IDLE
+ accessor.state.collect {
+ if (prev.refresh != it.refresh) {
+ loadStates.set(REFRESH, true, it.refresh)
+ dispatchIfValid(REFRESH, it.refresh)
+ }
+ if (prev.prepend != it.prepend) {
+ loadStates.set(PREPEND, true, it.prepend)
+ dispatchIfValid(PREPEND, it.prepend)
+ }
+ if (prev.append != it.append) {
+ loadStates.set(APPEND, true, it.append)
+ dispatchIfValid(APPEND, it.append)
+ }
+ prev = it
+ }
+ }
+
+ this@injectRemoteEvents.pageEventFlow.collect { event ->
+ when (event) {
+ is PageEvent.Insert -> {
+ loadStates.set(
+ sourceLoadStates = event.combinedLoadStates.source,
+ remoteLoadStates = accessor.state.value
+ )
+ send(event.copy(combinedLoadStates = loadStates.snapshot()))
+ }
+ is PageEvent.Drop -> {
+ loadStates.set(
+ type = event.loadType,
+ remote = false,
+ state = LoadState.NotLoading.Incomplete
+ )
+ send(event)
+ }
+ is PageEvent.LoadStateUpdate -> {
+ loadStates.set(
+ type = event.loadType,
+ remote = event.fromMediator,
+ state = event.loadState
+ )
+ send(event)
+ }
+ }
}
}
}
@@ -177,7 +208,7 @@
refreshEvents.send(false)
}
- private fun generateNewPagingSource(
+ private suspend fun generateNewPagingSource(
previousPagingSource: PagingSource<Key, Value>?
): PagingSource<Key, Value> {
val pagingSource = pagingSourceFactory()
diff --git a/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshot.kt b/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshot.kt
index 0d2dee2..020ae17 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshot.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshot.kt
@@ -303,19 +303,13 @@
if (result.prevKey == null) {
state.setSourceLoadState(
type = PREPEND,
- newState = when (remoteMediatorConnection) {
- null -> NotLoading.Complete
- else -> NotLoading.Incomplete
- }
+ newState = NotLoading.Complete
)
}
if (result.nextKey == null) {
state.setSourceLoadState(
type = APPEND,
- newState = when (remoteMediatorConnection) {
- null -> NotLoading.Complete
- else -> NotLoading.Incomplete
- }
+ newState = NotLoading.Complete
)
}
}
diff --git a/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshotState.kt b/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshotState.kt
index 147618c..36c0421 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshotState.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PageFetcherSnapshotState.kt
@@ -27,7 +27,6 @@
import androidx.paging.PagingConfig.Companion.MAX_SIZE_UNBOUNDED
import androidx.paging.PagingSource.LoadResult.Page
import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.consumeAsFlow
@@ -109,13 +108,11 @@
internal var sourceLoadStates = LoadStates.IDLE
private set
- @OptIn(ExperimentalCoroutinesApi::class)
fun consumePrependGenerationIdAsFlow(): Flow<Int> {
return prependGenerationIdCh.consumeAsFlow()
.onStart { prependGenerationIdCh.offer(prependGenerationId) }
}
- @OptIn(ExperimentalCoroutinesApi::class)
fun consumeAppendGenerationIdAsFlow(): Flow<Int> {
return appendGenerationIdCh.consumeAsFlow()
.onStart { appendGenerationIdCh.offer(appendGenerationId) }
@@ -152,24 +149,33 @@
placeholdersBefore = placeholdersBefore,
placeholdersAfter = placeholdersAfter,
combinedLoadStates = CombinedLoadStates(
+ refresh = sourceLoadStates.refresh,
+ prepend = sourceLoadStates.prepend,
+ append = sourceLoadStates.append,
source = sourceLoadStates,
- mediator = null
+ mediator = null,
)
)
PREPEND -> Prepend(
pages = pages,
placeholdersBefore = placeholdersBefore,
combinedLoadStates = CombinedLoadStates(
+ refresh = sourceLoadStates.refresh,
+ prepend = sourceLoadStates.prepend,
+ append = sourceLoadStates.append,
source = sourceLoadStates,
- mediator = null
+ mediator = null,
)
)
APPEND -> Append(
pages = pages,
placeholdersAfter = placeholdersAfter,
combinedLoadStates = CombinedLoadStates(
+ refresh = sourceLoadStates.refresh,
+ prepend = sourceLoadStates.prepend,
+ append = sourceLoadStates.append,
source = sourceLoadStates,
- mediator = null
+ mediator = null,
)
)
}
diff --git a/paging/common/src/main/kotlin/androidx/paging/PagedList.kt b/paging/common/src/main/kotlin/androidx/paging/PagedList.kt
index f54c929..2b22813 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PagedList.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PagedList.kt
@@ -26,7 +26,6 @@
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withContext
import java.lang.ref.WeakReference
import java.util.AbstractList
import java.util.concurrent.Executor
@@ -181,9 +180,7 @@
config.enablePlaceholders,
)
runBlocking {
- val initialResult = withContext(DirectDispatcher) {
- pagingSource.load(params)
- }
+ val initialResult = pagingSource.load(params)
when (initialResult) {
is PagingSource.LoadResult.Page -> initialResult
is PagingSource.LoadResult.Error -> throw initialResult.throwable
@@ -479,8 +476,11 @@
@Suppress("DEPRECATION")
fun build(): PagedList<Value> {
val fetchDispatcher = fetchDispatcher ?: Dispatchers.IO
- val pagingSource = pagingSource ?: dataSource?.let {
- LegacyPagingSource { it }.also {
+ val pagingSource = pagingSource ?: dataSource?.let { dataSource ->
+ LegacyPagingSource(
+ fetchDispatcher = fetchDispatcher,
+ dataSource = dataSource
+ ).also {
it.setPageSize(config.pageSize)
}
}
diff --git a/paging/common/src/main/kotlin/androidx/paging/Pager.kt b/paging/common/src/main/kotlin/androidx/paging/Pager.kt
index ed3cfda..8f2483d 100644
--- a/paging/common/src/main/kotlin/androidx/paging/Pager.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/Pager.kt
@@ -38,22 +38,41 @@
* `androidx.paging:paging-rxjava2` artifact.
*/
class Pager<Key : Any, Value : Any>
-@JvmOverloads constructor(
+// Experimental usage is propagated to public API via constructor argument.
+@ExperimentalPagingApi constructor(
config: PagingConfig,
initialKey: Key? = null,
- @OptIn(ExperimentalPagingApi::class)
- remoteMediator: RemoteMediator<Key, Value>? = null,
+ remoteMediator: RemoteMediator<Key, Value>?,
pagingSourceFactory: () -> PagingSource<Key, Value>
) {
+ // Experimental usage is internal, so opt-in is allowed here.
+ @JvmOverloads
+ @OptIn(ExperimentalPagingApi::class)
+ constructor(
+ config: PagingConfig,
+ initialKey: Key? = null,
+ pagingSourceFactory: () -> PagingSource<Key, Value>
+ ) : this(config, initialKey, null, pagingSourceFactory)
+
/**
* A cold [Flow] of [PagingData], which emits new instances of [PagingData] once they become
* invalidated by [PagingSource.invalidate] or calls to [AsyncPagingDataDiffer.refresh] or
* [PagingDataAdapter.refresh].
*/
val flow: Flow<PagingData<Value>> = PageFetcher(
- pagingSourceFactory,
- initialKey,
- config,
- remoteMediator
+ pagingSourceFactory = if (
+ pagingSourceFactory is SuspendingPagingSourceFactory<Key, Value>
+ ) {
+ pagingSourceFactory::create
+ } else {
+ // cannot pass it as is since it is not a suspend function. Hence, we wrap it in {}
+ // which means we are calling the original factory inside a suspend function
+ {
+ pagingSourceFactory()
+ }
+ },
+ initialKey = initialKey,
+ config = config,
+ remoteMediator = remoteMediator
).flow
}
diff --git a/paging/common/src/main/kotlin/androidx/paging/PagingData.kt b/paging/common/src/main/kotlin/androidx/paging/PagingData.kt
index 2f842b7..61548a2 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PagingData.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PagingData.kt
@@ -143,13 +143,15 @@
placeholdersBefore = 0,
placeholdersAfter = 0,
combinedLoadStates = CombinedLoadStates(
+ refresh = LoadState.NotLoading.Incomplete,
+ prepend = LoadState.NotLoading.Complete,
+ append = LoadState.NotLoading.Complete,
source = LoadStates(
refresh = LoadState.NotLoading.Incomplete,
prepend = LoadState.NotLoading.Complete,
append = LoadState.NotLoading.Complete
)
)
-
)
),
receiver = NOOP_RECEIVER
diff --git a/paging/common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt b/paging/common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt
index 90bb25e..3f93b05 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt
@@ -24,7 +24,6 @@
import androidx.paging.PagePresenter.ProcessPageEventCallback
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -103,6 +102,13 @@
}
/**
+ * @param onListPresentable Call this synchronously right before dispatching updates to signal
+ * that this [PagingDataDiffer] should now consider [newList] as the presented list for
+ * presenter-level APIs such as [snapshot] and [peek]. This should be called before notifying
+ * any callbacks that the user would expect to be synchronous with presenter updates, such as
+ * `ListUpdateCallback`, in case it's desirable to inspect presenter state within those
+ * callbacks.
+ *
* @return Transformed result of [lastAccessedIndex] as an index of [newList] using the diff
* result between [previousList] and [newList]. Null if [newList] or [previousList] lists are
* empty, where it does not make sense to transform [lastAccessedIndex].
@@ -111,7 +117,8 @@
previousList: NullPaddedList<T>,
newList: NullPaddedList<T>,
newCombinedLoadStates: CombinedLoadStates,
- lastAccessedIndex: Int
+ lastAccessedIndex: Int,
+ onListPresentable: () -> Unit,
): Int?
open fun postEvents(): Boolean = false
@@ -126,13 +133,23 @@
lastAccessedIndexUnfulfilled = false
val newPresenter = PagePresenter(event)
+ var onListPresentableCalled = false
val transformedLastAccessedIndex = presentNewList(
previousList = presenter,
newList = newPresenter,
newCombinedLoadStates = event.combinedLoadStates,
- lastAccessedIndex = lastAccessedIndex
+ lastAccessedIndex = lastAccessedIndex,
+ onListPresentable = {
+ presenter = newPresenter
+ onListPresentableCalled = true
+ }
)
- presenter = newPresenter
+ check(onListPresentableCalled) {
+ "Missing call to onListPresentable after new list was presented. If you " +
+ "are seeing this exception, it is generally an indication of an " +
+ "issue with Paging. Please file a bug so we can fix it at: " +
+ "https://issuetracker.google.com/issues/new?component=413106"
+ }
// Dispatch LoadState updates as soon as we are done diffing, but after setting
// presenter.
@@ -277,7 +294,6 @@
val size: Int
get() = presenter.size
- @OptIn(ExperimentalCoroutinesApi::class)
private val _combinedLoadState = MutableStateFlow(combinedLoadStates.snapshot())
/**
@@ -294,7 +310,6 @@
get() = _combinedLoadState
init {
- @OptIn(ExperimentalCoroutinesApi::class)
addLoadStateListener {
_combinedLoadState.value = it
}
diff --git a/paging/common/src/main/kotlin/androidx/paging/PagingSource.kt b/paging/common/src/main/kotlin/androidx/paging/PagingSource.kt
index 3f64938..4db4e26 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PagingSource.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PagingSource.kt
@@ -315,7 +315,6 @@
* list of loaded pages is not empty. In the case where a refresh is triggered before the
* initial load succeeds or it errors out, the initial key passed to [Pager] will be used.
*/
- @ExperimentalPagingApi
open fun getRefreshKey(state: PagingState<Key, Value>): Key? = null
private val onInvalidatedCallbacks = CopyOnWriteArrayList<() -> Unit>()
@@ -335,9 +334,7 @@
* This method is idempotent. i.e., If [invalidate] has already been called, subsequent calls to
* this method should have no effect.
*/
- open fun invalidate() {
- // TODO(b/137971356): Investigate making this not open when able to remove
- // LegacyPagingSource.
+ fun invalidate() {
if (_invalid.compareAndSet(false, true)) {
onInvalidatedCallbacks.forEach { it.invoke() }
}
diff --git a/paging/common/src/main/kotlin/androidx/paging/Separators.kt b/paging/common/src/main/kotlin/androidx/paging/Separators.kt
index 94ed298..632f888 100644
--- a/paging/common/src/main/kotlin/androidx/paging/Separators.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/Separators.kt
@@ -16,10 +16,13 @@
package androidx.paging
+import androidx.paging.LoadState.NotLoading
import androidx.paging.LoadType.APPEND
import androidx.paging.LoadType.PREPEND
+import androidx.paging.LoadType.REFRESH
import androidx.paging.PageEvent.Drop
import androidx.paging.PageEvent.Insert
+import androidx.paging.PageEvent.LoadStateUpdate
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@@ -165,17 +168,18 @@
var endTerminalSeparatorDeferred = false
var startTerminalSeparatorDeferred = false
+ val loadStates = MutableLoadStateCollection()
+ var placeholdersBefore = 0
+ var placeholdersAfter = 0
+
var footerAdded = false
var headerAdded = false
@Suppress("UNCHECKED_CAST")
suspend fun onEvent(event: PageEvent<T>): PageEvent<R> = when (event) {
is Insert<T> -> onInsert(event)
- is Drop -> {
- onDrop(event) // Update pageStash state
- event as Drop<R>
- }
- is PageEvent.LoadStateUpdate -> event as PageEvent<R>
+ is Drop -> onDrop(event)
+ is LoadStateUpdate -> onLoadStateUpdate(event)
}.also {
// validate internal state after each modification
if (endTerminalSeparatorDeferred) {
@@ -191,16 +195,14 @@
return this as Insert<R>
}
- fun LoadState.isTerminal(): Boolean {
- return this is LoadState.NotLoading && endOfPaginationReached
- }
-
fun CombinedLoadStates.terminatesStart(): Boolean {
- return source.prepend.isTerminal() && mediator?.prepend?.isTerminal() != false
+ return source.prepend.endOfPaginationReached &&
+ mediator?.prepend?.endOfPaginationReached != false
}
fun CombinedLoadStates.terminatesEnd(): Boolean {
- return source.append.isTerminal() && mediator?.append?.isTerminal() != false
+ return source.append.endOfPaginationReached &&
+ mediator?.append?.endOfPaginationReached != false
}
fun <T : Any> Insert<T>.terminatesStart(): Boolean = if (loadType == APPEND) {
@@ -227,6 +229,17 @@
"Additional append event after append state is done"
}
+ // Update SeparatorState before we do any real work.
+ loadStates.set(event.combinedLoadStates)
+ // Append insert has placeholdersBefore = -1 as a placeholder value.
+ if (event.loadType != APPEND) {
+ placeholdersBefore = event.placeholdersBefore
+ }
+ // Prepend insert has placeholdersAfter = -1 as a placeholder value.
+ if (event.loadType != PREPEND) {
+ placeholdersAfter = event.placeholdersAfter
+ }
+
if (eventEmpty) {
if (eventTerminatesStart && eventTerminatesEnd) {
// if event is empty, and fully terminal, resolve single separator, and that's it
@@ -410,7 +423,16 @@
/**
* Process a [Drop] event to update [pageStash] stage.
*/
- fun onDrop(event: Drop<T>) {
+ fun onDrop(event: Drop<T>): Drop<R> {
+ loadStates.set(type = event.loadType, remote = false, state = NotLoading.Incomplete)
+ if (event.loadType == PREPEND) {
+ placeholdersBefore = event.placeholdersRemaining
+ headerAdded = false
+ } else if (event.loadType == APPEND) {
+ placeholdersAfter = event.placeholdersRemaining
+ footerAdded = false
+ }
+
if (pageStash.isEmpty()) {
if (event.loadType == PREPEND) {
startTerminalSeparatorDeferred = false
@@ -424,6 +446,49 @@
pageStash.removeAll { stash ->
stash.originalPageOffsets.any { pageOffsetsToDrop.contains(it) }
}
+
+ @Suppress("UNCHECKED_CAST")
+ return event as Drop<R>
+ }
+
+ suspend fun onLoadStateUpdate(event: LoadStateUpdate<T>): PageEvent<R> {
+ // Check for redundant LoadStateUpdate events to avoid unnecessary mapping to empty inserts
+ // that might cause terminal separators to get added out of place.
+ if (loadStates.get(event.loadType, event.fromMediator) == event.loadState) {
+ @Suppress("UNCHECKED_CAST")
+ return event as PageEvent<R>
+ }
+
+ loadStates.set(type = event.loadType, remote = event.fromMediator, state = event.loadState)
+
+ // Transform terminal load state updates into empty inserts for header + footer support
+ // when used with RemoteMediator. In cases where we defer adding a terminal separator,
+ // RemoteMediator can report endOfPaginationReached via LoadStateUpdate event, which
+ // isn't possible to add a separator to. Note: Adding a separate insert event also
+ // doesn't work in the case where .insertSeparators() is called multiple times on the
+ // same page event stream - we have to transform the terminating LoadStateUpdate event.
+ if (event.loadType != REFRESH && event.fromMediator &&
+ event.loadState.endOfPaginationReached
+ ) {
+ val emptyTerminalInsert: Insert<T> = if (event.loadType == PREPEND) {
+ Insert.Prepend(
+ pages = emptyList(),
+ placeholdersBefore = placeholdersBefore,
+ combinedLoadStates = loadStates.snapshot(),
+ )
+ } else {
+ Insert.Append(
+ pages = emptyList(),
+ placeholdersAfter = placeholdersAfter,
+ combinedLoadStates = loadStates.snapshot(),
+ )
+ }
+
+ return onInsert(emptyTerminalInsert)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ return event as PageEvent<R>
}
private fun <T : Any> transformablePageToStash(
diff --git a/paging/common/src/main/kotlin/androidx/paging/SuspendingPagingSourceFactory.kt b/paging/common/src/main/kotlin/androidx/paging/SuspendingPagingSourceFactory.kt
new file mode 100644
index 0000000..1b15542f
--- /dev/null
+++ b/paging/common/src/main/kotlin/androidx/paging/SuspendingPagingSourceFactory.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.paging
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/**
+ * Utility class to convert the paging source factory to a suspend one.
+ *
+ * This is internal because it is only necessary for the legacy paging source implementation
+ * where the data source must be created on the given thread pool for API guarantees.
+ * see: b/173029013
+ * see: b/168061354
+ */
+internal class SuspendingPagingSourceFactory<Key : Any, Value : Any>(
+ private val dispatcher: CoroutineDispatcher,
+ private val delegate: () -> PagingSource<Key, Value>
+) : () -> PagingSource<Key, Value> {
+ suspend fun create(): PagingSource<Key, Value> {
+ return withContext(dispatcher) {
+ delegate()
+ }
+ }
+
+ override fun invoke(): PagingSource<Key, Value> {
+ return delegate()
+ }
+}
diff --git a/paging/common/src/main/kotlin/androidx/paging/multicast/Multicaster.kt b/paging/common/src/main/kotlin/androidx/paging/multicast/Multicaster.kt
index 18d85e5..b2073b5 100644
--- a/paging/common/src/main/kotlin/androidx/paging/multicast/Multicaster.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/multicast/Multicaster.kt
@@ -17,7 +17,6 @@
package androidx.paging.multicast
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.consumeAsFlow
@@ -72,8 +71,7 @@
)
}
- @OptIn(ExperimentalCoroutinesApi::class)
- val flow = flow<T> {
+ val flow: Flow<T> = flow {
val channel = Channel<ChannelManager.Message.Dispatch.Value<T>>(Channel.UNLIMITED)
val subFlow = channel.consumeAsFlow()
.onStart {
diff --git a/paging/common/src/test/kotlin/androidx/paging/ContiguousPagedListTest.kt b/paging/common/src/test/kotlin/androidx/paging/ContiguousPagedListTest.kt
index a6941fc..03811c2 100644
--- a/paging/common/src/test/kotlin/androidx/paging/ContiguousPagedListTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/ContiguousPagedListTest.kt
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-@file:OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
@file:Suppress("DEPRECATION")
package androidx.paging
@@ -62,9 +61,9 @@
* and alignment restrictions. These tests were written before positional+contiguous enforced
* these behaviors.
*/
- private inner class TestPagingSource(val listData: List<Item> = ITEMS) :
- PagingSource<Int, Item>() {
- @OptIn(ExperimentalPagingApi::class)
+ private inner class TestPagingSource(
+ val listData: List<Item> = ITEMS
+ ) : PagingSource<Int, Item>() {
override fun getRefreshKey(state: PagingState<Int, Item>): Int? {
return state.anchorPosition
?.let { anchorPosition -> state.closestItemToPosition(anchorPosition)?.pos }
@@ -313,10 +312,6 @@
}
}
- private fun verifyDropCallback(callback: Callback, position: Int) {
- verifyDropCallback(callback, position, position)
- }
-
@Test
fun append() {
val pagedList = createCountedPagedList(0)
@@ -463,7 +458,7 @@
drain()
verifyRange(20, 60, pagedList)
verifyCallback(callback, 60)
- verifyDropCallback(callback, 0)
+ verifyDropCallback(callback, 0, 0)
verifyNoMoreInteractions(callback)
}
@@ -1050,10 +1045,10 @@
assertTrue { mainThread.queue.isEmpty() }
- pagedList.dispatchStateChangeAsync(LoadType.REFRESH, LoadState.Loading)
+ pagedList.dispatchStateChangeAsync(LoadType.REFRESH, Loading)
assertEquals(1, mainThread.queue.size)
- pagedList.dispatchStateChangeAsync(LoadType.REFRESH, LoadState.NotLoading.Incomplete)
+ pagedList.dispatchStateChangeAsync(LoadType.REFRESH, NotLoading.Incomplete)
assertEquals(2, mainThread.queue.size)
}
diff --git a/paging/common/src/test/kotlin/androidx/paging/ItemKeyedDataSourceTest.kt b/paging/common/src/test/kotlin/androidx/paging/ItemKeyedDataSourceTest.kt
index 5448b6d..13182ab 100644
--- a/paging/common/src/test/kotlin/androidx/paging/ItemKeyedDataSourceTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/ItemKeyedDataSourceTest.kt
@@ -19,6 +19,7 @@
import androidx.paging.PagingSource.LoadResult.Page.Companion.COUNT_UNDEFINED
import com.nhaarman.mockitokotlin2.capture
import com.nhaarman.mockitokotlin2.mock
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
@@ -289,7 +290,7 @@
@Suppress("DEPRECATION")
PagedList.Builder(dataSource, 10)
.setNotifyDispatcher(FailDispatcher())
- .setFetchDispatcher(DirectDispatcher)
+ .setFetchDispatcher(Dispatchers.IO)
.setInitialKey("")
.build()
}
diff --git a/paging/common/src/test/kotlin/androidx/paging/LegacyPageFetcherTest.kt b/paging/common/src/test/kotlin/androidx/paging/LegacyPageFetcherTest.kt
index b93aea5..6e8ede2 100644
--- a/paging/common/src/test/kotlin/androidx/paging/LegacyPageFetcherTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/LegacyPageFetcherTest.kt
@@ -139,7 +139,7 @@
GlobalScope,
config,
pagingSource,
- DirectDispatcher,
+ testDispatcher,
testDispatcher,
consumer,
storage as LegacyPageFetcher.KeyProvider<Int>
diff --git a/paging/common/src/test/kotlin/androidx/paging/LegacyPagingSourceTest.kt b/paging/common/src/test/kotlin/androidx/paging/LegacyPagingSourceTest.kt
index 5b8ea29..add2986 100644
--- a/paging/common/src/test/kotlin/androidx/paging/LegacyPagingSourceTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/LegacyPagingSourceTest.kt
@@ -17,20 +17,23 @@
package androidx.paging
import androidx.paging.PagingSource.LoadResult.Page
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.Runnable
-import kotlinx.coroutines.launch
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-import kotlin.coroutines.CoroutineContext
+import java.util.concurrent.Executors
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
-@OptIn(ExperimentalPagingApi::class)
@RunWith(JUnit4::class)
class LegacyPagingSourceTest {
private val fakePagingState = PagingState(
@@ -76,7 +79,10 @@
override fun getKey(item: String) = item.hashCode()
}
- val pagingSource = LegacyPagingSource { dataSource }
+ val pagingSource = LegacyPagingSource(
+ fetchDispatcher = Dispatchers.Unconfined,
+ dataSource
+ )
// Check that jumpingSupported is disabled.
assertFalse { pagingSource.jumpingSupported }
@@ -119,7 +125,10 @@
Assert.fail("loadAfter not expected")
}
}
- val pagingSource = LegacyPagingSource { dataSource }
+ val pagingSource = LegacyPagingSource(
+ fetchDispatcher = Dispatchers.Unconfined,
+ dataSource = dataSource
+ )
// Check that jumpingSupported is disabled.
assertFalse { pagingSource.jumpingSupported }
@@ -139,7 +148,10 @@
@Test
fun positional() {
- val pagingSource = LegacyPagingSource { createTestPositionalDataSource() }
+ val pagingSource = LegacyPagingSource(
+ fetchDispatcher = Dispatchers.Unconfined,
+ dataSource = createTestPositionalDataSource()
+ )
// Check that jumpingSupported is enabled.
assertTrue { pagingSource.jumpingSupported }
@@ -189,7 +201,10 @@
@Test
fun invalidateFromPagingSource() {
- val pagingSource = LegacyPagingSource { createTestPositionalDataSource() }
+ val pagingSource = LegacyPagingSource(
+ fetchDispatcher = Dispatchers.Unconfined,
+ dataSource = createTestPositionalDataSource()
+ )
val dataSource = pagingSource.dataSource
var kotlinInvalidated = false
@@ -216,7 +231,10 @@
@Test
fun invalidateFromDataSource() {
- val pagingSource = LegacyPagingSource { createTestPositionalDataSource() }
+ val pagingSource = LegacyPagingSource(
+ fetchDispatcher = Dispatchers.Unconfined,
+ dataSource = createTestPositionalDataSource()
+ )
val dataSource = pagingSource.dataSource
var kotlinInvalidated = false
@@ -241,41 +259,57 @@
assertTrue { javaInvalidated }
}
+ @Suppress("DEPRECATION")
@Test
fun createDataSourceOnFetchDispatcher() {
- val manualDispatcher = object : CoroutineDispatcher() {
- val coroutines = ArrayList<Pair<CoroutineContext, Runnable>>()
- override fun dispatch(context: CoroutineContext, block: Runnable) {
- coroutines.add(context to block)
+ val methodCalls = mutableMapOf<String, MutableList<Thread>>()
+
+ val dataSourceFactory = object : DataSource.Factory<Int, String>() {
+ override fun create(): DataSource<Int, String> {
+ return ThreadCapturingDataSource { methodName ->
+ methodCalls.getOrPut(methodName) {
+ mutableListOf()
+ }.add(Thread.currentThread())
+ }
}
}
- var initialized = false
- val pagingSource = LegacyPagingSource(manualDispatcher) {
- initialized = true
- createTestPositionalDataSource(expectInitialLoad = true)
- }
+ // create an executor special to the legacy data source
+ val executor = Executors.newSingleThreadExecutor()
- assertFalse { initialized }
+ // extract the thread instance from the executor. we'll use it to assert calls later
+ var dataSourceThread: Thread? = null
+ executor.submit {
+ dataSourceThread = Thread.currentThread()
+ }.get()
- // Trigger lazy-initialization dispatch.
- val job = GlobalScope.launch {
- pagingSource.load(PagingSource.LoadParams.Refresh(0, 1, false))
- }
-
- // Assert that initialization has been scheduled on manualDispatcher, which has not been
- // triggered yet.
- assertFalse { initialized }
-
- // Force all tasks on manualDispatcher to run.
- while (!job.isCompleted) {
- while (manualDispatcher.coroutines.isNotEmpty()) {
- @OptIn(ExperimentalStdlibApi::class)
- manualDispatcher.coroutines.removeFirst().second.run()
+ val pager = Pager(
+ config = PagingConfig(10, enablePlaceholders = false),
+ pagingSourceFactory = dataSourceFactory.asPagingSourceFactory(
+ executor.asCoroutineDispatcher()
+ )
+ )
+ // collect from pager. we take only 2 paging data generations and only take 1 PageEvent
+ // from them
+ runBlocking {
+ pager.flow.take(2).collectLatest { pagingData ->
+ // wait until first insert happens
+ pagingData.flow.filter {
+ it is PageEvent.Insert
+ }.first()
+ pagingData.receiver.refresh()
}
}
-
- assertTrue { initialized }
+ // validate method calls (to ensure test did run as expected) and their threads.
+ assertThat(methodCalls["<init>"]).hasSize(2)
+ assertThat(methodCalls["<init>"]?.toSet()).containsExactly(dataSourceThread)
+ assertThat(methodCalls["addInvalidatedCallback"]).hasSize(2)
+ assertThat(methodCalls["addInvalidatedCallback"]?.toSet()).containsExactly(dataSourceThread)
+ assertThat(methodCalls["loadInitial"]).hasSize(2)
+ assertThat(methodCalls).containsKey("isInvalid")
+ assertThat(methodCalls["loadInitial"]?.toSet()).containsExactly(dataSourceThread)
+ // TODO b/174625633 this should also be 2
+ assertThat(methodCalls["removeInvalidatedCallback"]).hasSize(1)
}
@Test
@@ -333,4 +367,55 @@
Assert.fail("loadRange not expected")
}
}
+
+ /**
+ * A data source implementation which tracks method calls and their threads.
+ */
+ @Suppress("DEPRECATION")
+ class ThreadCapturingDataSource(
+ private val recordMethodCall: (methodName: String) -> Unit
+ ) : PositionalDataSource<String>() {
+ init {
+ recordMethodCall("<init>")
+ }
+
+ override fun loadInitial(
+ params: LoadInitialParams,
+ callback: LoadInitialCallback<String>
+ ) {
+ recordMethodCall("loadInitial")
+ callback.onResult(
+ data = emptyList(),
+ position = 0,
+ )
+ }
+
+ override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<String>) {
+ recordMethodCall("loadRange")
+ callback.onResult(data = emptyList())
+ }
+
+ override val isInvalid: Boolean
+ get() {
+ // this is important because room's implementation might run a db query to
+ // update invalidations.
+ recordMethodCall("isInvalid")
+ return super.isInvalid
+ }
+
+ override fun addInvalidatedCallback(onInvalidatedCallback: InvalidatedCallback) {
+ recordMethodCall("addInvalidatedCallback")
+ super.addInvalidatedCallback(onInvalidatedCallback)
+ }
+
+ override fun removeInvalidatedCallback(onInvalidatedCallback: InvalidatedCallback) {
+ recordMethodCall("removeInvalidatedCallback")
+ super.removeInvalidatedCallback(onInvalidatedCallback)
+ }
+
+ override fun invalidate() {
+ recordMethodCall("invalidate")
+ super.invalidate()
+ }
+ }
}
diff --git a/paging/common/src/test/kotlin/androidx/paging/PageFetcherSnapshotTest.kt b/paging/common/src/test/kotlin/androidx/paging/PageFetcherSnapshotTest.kt
index 21736e3..232c4f7 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PageFetcherSnapshotTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PageFetcherSnapshotTest.kt
@@ -72,11 +72,12 @@
class PageFetcherSnapshotTest {
private val testScope = TestCoroutineScope()
private val retryBus = ConflatedEventBus<Unit>()
- private val pagingSourceFactory = {
- TestPagingSource().also {
+ private val pagingSourceFactory = suspend {
+ TestPagingSource(loadDelay = 1000).also {
currentPagingSource = it
}
}
+
private var currentPagingSource: TestPagingSource? = null
private val config = PagingConfig(
pageSize = 1,
@@ -1752,7 +1753,7 @@
}
var createdPagingSource = false
- val factory = {
+ val factory = suspend {
check(!createdPagingSource)
createdPagingSource = true
TestPagingSource(items = List(2) { it })
@@ -1801,7 +1802,7 @@
}
var createdPagingSource = false
- val factory = {
+ val factory = suspend {
check(!createdPagingSource)
createdPagingSource = true
TestPagingSource(items = List(2) { it })
@@ -2183,7 +2184,14 @@
LoadStateUpdate(REFRESH, true, Loading),
LoadStateUpdate(REFRESH, true, Error(EXCEPTION)),
LoadStateUpdate(REFRESH, false, Loading),
- createRefresh(0..2, remoteLoadStatesOf(refreshRemote = Error(EXCEPTION))),
+ createRefresh(
+ range = 0..2,
+ remoteLoadStatesOf(
+ refresh = Error(EXCEPTION),
+ prependLocal = NotLoading.Complete,
+ refreshRemote = Error(EXCEPTION),
+ ),
+ ),
// since remote refresh failed and launch initial refresh is requested,
// we won't receive any append/prepend events
)
@@ -2262,6 +2270,79 @@
}
@Test
+ fun remoteMediator_remoteRefreshEndOfPaginationReached() = testScope.runBlockingTest {
+ @OptIn(ExperimentalPagingApi::class)
+ val remoteMediator = RemoteMediatorMock().apply {
+ initializeResult = RemoteMediator.InitializeAction.LAUNCH_INITIAL_REFRESH
+ loadCallback = { _, _ -> RemoteMediator.MediatorResult.Success(true) }
+ }
+
+ val config = PagingConfig(
+ pageSize = 1,
+ prefetchDistance = 2,
+ enablePlaceholders = true,
+ initialLoadSize = 1,
+ maxSize = 5
+ )
+ val pager = PageFetcher(
+ initialKey = 0,
+ pagingSourceFactory = { TestPagingSource(items = listOf(0)) },
+ config = config,
+ remoteMediator = remoteMediator
+ )
+
+ val state = collectFetcherState(pager)
+
+ advanceUntilIdle()
+ assertThat(state.newEvents()).isEqualTo(
+ listOf(
+ LoadStateUpdate(loadType = REFRESH, fromMediator = true, loadState = Loading),
+ LoadStateUpdate(
+ loadType = REFRESH,
+ fromMediator = true,
+ loadState = NotLoading(endOfPaginationReached = true)
+ ),
+ LoadStateUpdate(
+ loadType = PREPEND,
+ fromMediator = true,
+ loadState = NotLoading(endOfPaginationReached = true)
+ ),
+ LoadStateUpdate(
+ loadType = APPEND,
+ fromMediator = true,
+ loadState = NotLoading(endOfPaginationReached = true)
+ ),
+ LoadStateUpdate(loadType = REFRESH, fromMediator = false, loadState = Loading),
+ Refresh(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffsets = intArrayOf(0),
+ data = listOf(0),
+ hintOriginalPageOffset = 0,
+ hintOriginalIndices = null
+ )
+ ),
+ placeholdersBefore = 0,
+ placeholdersAfter = 0,
+ combinedLoadStates = remoteLoadStatesOf(
+ refresh = NotLoading(endOfPaginationReached = true),
+ prepend = NotLoading(endOfPaginationReached = true),
+ append = NotLoading(endOfPaginationReached = true),
+ refreshLocal = NotLoading(endOfPaginationReached = false),
+ prependLocal = NotLoading(endOfPaginationReached = true),
+ appendLocal = NotLoading(endOfPaginationReached = true),
+ refreshRemote = NotLoading(endOfPaginationReached = true),
+ prependRemote = NotLoading(endOfPaginationReached = true),
+ appendRemote = NotLoading(endOfPaginationReached = true),
+ )
+ )
+ )
+ )
+
+ state.job.cancel()
+ }
+
+ @Test
fun remoteMediator_endOfPaginationNotReachedLoadStatePrepend() = testScope.runBlockingTest {
@OptIn(ExperimentalPagingApi::class)
val remoteMediator = object : RemoteMediatorMock() {
@@ -2305,7 +2386,9 @@
),
placeholdersBefore = 0,
placeholdersAfter = 99,
- combinedLoadStates = remoteLoadStatesOf()
+ combinedLoadStates = remoteLoadStatesOf(
+ prependLocal = NotLoading.Complete
+ )
),
LoadStateUpdate(
loadType = PREPEND,
@@ -2328,7 +2411,9 @@
),
placeholdersBefore = 0,
placeholdersAfter = 99,
- combinedLoadStates = remoteLoadStatesOf()
+ combinedLoadStates = remoteLoadStatesOf(
+ prependLocal = NotLoading.Complete
+ )
)
)
@@ -2380,7 +2465,9 @@
),
placeholdersBefore = 0,
placeholdersAfter = 99,
- combinedLoadStates = remoteLoadStatesOf()
+ combinedLoadStates = remoteLoadStatesOf(
+ prependLocal = NotLoading.Complete,
+ )
),
LoadStateUpdate(
loadType = PREPEND,
@@ -2444,10 +2531,7 @@
combinedLoadStates = remoteLoadStatesOf()
)
)
- assertEvents(
- eventsByGeneration[0],
- refreshEvents
- )
+ assertThat(eventsByGeneration[0]).isEqualTo(refreshEvents)
accessHint(
ViewportHint.Access(
pageOffset = 0,
@@ -2485,7 +2569,7 @@
loadType = PREPEND,
fromMediator = true,
loadState = NotLoading.Complete
- )
+ ),
)
awaitEventCount(refreshEvents.size + postHintEvents.size)
assertEquals(
@@ -2539,7 +2623,9 @@
),
placeholdersBefore = 99,
placeholdersAfter = 0,
- combinedLoadStates = remoteLoadStatesOf()
+ combinedLoadStates = remoteLoadStatesOf(
+ appendLocal = NotLoading.Complete,
+ )
),
LoadStateUpdate(
loadType = APPEND,
@@ -2562,7 +2648,9 @@
),
placeholdersBefore = 99,
placeholdersAfter = 0,
- combinedLoadStates = remoteLoadStatesOf()
+ combinedLoadStates = remoteLoadStatesOf(
+ appendLocal = NotLoading.Complete,
+ )
),
)
)
@@ -2597,7 +2685,7 @@
)
val expected = listOf(
- listOf<PageEvent<Int>>(
+ listOf(
LoadStateUpdate(
loadType = REFRESH,
fromMediator = false,
@@ -2612,7 +2700,9 @@
),
placeholdersBefore = 99,
placeholdersAfter = 0,
- combinedLoadStates = remoteLoadStatesOf()
+ combinedLoadStates = remoteLoadStatesOf(
+ appendLocal = NotLoading.Complete,
+ )
),
LoadStateUpdate(
loadType = APPEND,
@@ -2716,7 +2806,7 @@
),
)
awaitEventCount(initialEvents.size + postHintEvents.size)
- assertEvents(initialEvents + postHintEvents, eventsByGeneration[0])
+ assertThat(eventsByGeneration[0]).isEqualTo(initialEvents + postHintEvents)
}
}
@@ -2760,8 +2850,13 @@
fromMediator = true,
loadState = Loading
),
+ LoadStateUpdate(
+ loadType = REFRESH,
+ fromMediator = true,
+ loadState = NotLoading.Incomplete
+ ),
),
- listOf<PageEvent<Int>>(
+ listOf(
LoadStateUpdate(
loadType = REFRESH,
fromMediator = false,
@@ -2786,7 +2881,7 @@
@Test
fun remoteMediator_initialRefreshSuccessEndOfPagination() = testScope.runBlockingTest {
@OptIn(ExperimentalPagingApi::class)
- val remoteMediator = object : RemoteMediatorMock() {
+ val remoteMediator = object : RemoteMediatorMock(loadDelay = 2000) {
override suspend fun initialize(): InitializeAction {
super.initialize()
return InitializeAction.LAUNCH_INITIAL_REFRESH
@@ -2810,57 +2905,79 @@
)
val pager = PageFetcher(
initialKey = 50,
- pagingSourceFactory = pagingSourceFactory,
+ pagingSourceFactory = {
+ TestPagingSource().apply {
+ nextLoadResult = Page(
+ data = listOf(50),
+ prevKey = null,
+ nextKey = null,
+ itemsBefore = 50,
+ itemsAfter = 49
+ )
+ }
+ },
config = config,
remoteMediator = remoteMediator
)
- pager.assertEventByGeneration(
+ val fetcherState = collectFetcherState(pager)
+
+ advanceTimeBy(1000)
+
+ assertThat(fetcherState.newEvents()).isEqualTo(
listOf(
- listOf(
- LoadStateUpdate(
- loadType = REFRESH,
- fromMediator = true,
- loadState = Loading,
- ),
- LoadStateUpdate(
- loadType = REFRESH,
- fromMediator = true,
- loadState = NotLoading.Complete,
- ),
- LoadStateUpdate(
- loadType = PREPEND,
- fromMediator = true,
- loadState = NotLoading.Complete,
- ),
- LoadStateUpdate(
- loadType = APPEND,
- fromMediator = true,
- loadState = NotLoading.Complete,
- ),
- LoadStateUpdate(
- loadType = REFRESH,
- fromMediator = false,
- loadState = Loading,
- ),
- Refresh(
- pages = listOf(
- TransformablePage(
- originalPageOffset = 0,
- data = listOf(50)
- )
- ),
- placeholdersBefore = 50,
- placeholdersAfter = 49,
- combinedLoadStates = remoteLoadStatesOf(
- refreshRemote = NotLoading.Complete,
- prependRemote = NotLoading.Complete,
- appendRemote = NotLoading.Complete,
+ LoadStateUpdate(
+ loadType = REFRESH,
+ fromMediator = true,
+ loadState = Loading,
+ ),
+ LoadStateUpdate(
+ loadType = REFRESH,
+ fromMediator = false,
+ loadState = Loading,
+ ),
+ Refresh(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffset = 0,
+ data = listOf(50)
)
),
+ placeholdersBefore = 50,
+ placeholdersAfter = 49,
+ combinedLoadStates = remoteLoadStatesOf(
+ refresh = Loading,
+ prependLocal = NotLoading.Complete,
+ appendLocal = NotLoading.Complete,
+ refreshRemote = Loading,
+ )
+ ),
+ ),
+ )
+
+ advanceUntilIdle()
+
+ assertThat(fetcherState.newEvents()).isEqualTo(
+ listOf<PageEvent<Int>>(
+ LoadStateUpdate(
+ loadType = REFRESH,
+ fromMediator = true,
+ loadState = NotLoading.Complete,
+ ),
+ LoadStateUpdate(
+ loadType = PREPEND,
+ fromMediator = true,
+ loadState = NotLoading.Complete
+ ),
+ LoadStateUpdate(
+ loadType = APPEND,
+ fromMediator = true,
+ loadState = NotLoading.Complete
),
)
)
+
+ fetcherState.job.cancel()
}
@Test
@@ -3225,15 +3342,6 @@
assertFalse { initialHint.shouldPrioritizeOver(accessHint, APPEND) }
}
- @OptIn(ExperimentalPagingApi::class)
- private suspend fun <Key : Any, Value : Any> createRemoteMediatorAccessor(
- delegate: RemoteMediator<Key, Value>
- ): RemoteMediatorAccessor<Key, Value> {
- return RemoteMediatorAccessor(testScope, delegate).also {
- it.initialize()
- }
- }
-
internal class CollectedPageEvents<T : Any>(val pageEvents: ArrayList<PageEvent<T>>) {
var lastIndex = 0
fun newEvents(): List<PageEvent<T>> = when {
@@ -3309,7 +3417,7 @@
stop()
}
expected.forEachIndexed { index, list ->
- assertEvents(list, actual.getOrNull(index) ?: emptyList())
+ assertThat(actual.getOrNull(index) ?: emptyList<PageEvent<T>>()).isEqualTo(list)
}
assertThat(actual.size).isEqualTo(expected.size)
}
diff --git a/paging/common/src/test/kotlin/androidx/paging/PageFetcherTest.kt b/paging/common/src/test/kotlin/androidx/paging/PageFetcherTest.kt
index b347092..4cb76d0 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PageFetcherTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PageFetcherTest.kt
@@ -49,7 +49,7 @@
@RunWith(JUnit4::class)
class PageFetcherTest {
private val testScope = TestCoroutineScope()
- private val pagingSourceFactory = { TestPagingSource() }
+ private val pagingSourceFactory = suspend { TestPagingSource() }
private val config = PagingConfig(
pageSize = 1,
prefetchDistance = 1,
@@ -91,7 +91,9 @@
@Test
fun refresh_fromPagingSource() = testScope.runBlockingTest {
var pagingSource: PagingSource<Int, Int>? = null
- val pagingSourceFactory = { TestPagingSource().also { pagingSource = it } }
+ val pagingSourceFactory = suspend {
+ TestPagingSource().also { pagingSource = it }
+ }
val pageFetcher = PageFetcher(pagingSourceFactory, 50, config)
val fetcherState = collectFetcherState(pageFetcher)
@@ -114,7 +116,9 @@
@Test
fun refresh_callsInvalidate() = testScope.runBlockingTest {
var pagingSource: PagingSource<Int, Int>? = null
- val pagingSourceFactory = { TestPagingSource().also { pagingSource = it } }
+ val pagingSourceFactory = suspend {
+ TestPagingSource().also { pagingSource = it }
+ }
val pageFetcher = PageFetcher(pagingSourceFactory, 50, config)
val fetcherState = collectFetcherState(pageFetcher)
@@ -303,7 +307,9 @@
fun jump() = testScope.runBlockingTest {
pauseDispatcher {
val pagingSources = mutableListOf<PagingSource<Int, Int>>()
- val pagingSourceFactory = { TestPagingSource().also { pagingSources.add(it) } }
+ val pagingSourceFactory = suspend {
+ TestPagingSource().also { pagingSources.add(it) }
+ }
val config = PagingConfig(
pageSize = 1,
prefetchDistance = 1,
@@ -382,7 +388,11 @@
prefetchDistance = 1,
initialLoadSize = 2
)
- val pageFetcher = PageFetcher({ pagingSource }, 50, config)
+ val pageFetcher = PageFetcher(
+ pagingSourceFactory = suspend { pagingSource },
+ initialKey = 50,
+ config = config
+ )
val job = testScope.launch {
assertFailsWith<IllegalStateException> {
pageFetcher.flow.collect { }
@@ -444,7 +454,7 @@
)
val pagingSources = mutableListOf<TestPagingSource>()
val pageFetcher = PageFetcher(
- pagingSourceFactory = {
+ pagingSourceFactory = suspend {
TestPagingSource(loadDelay = 1000).also {
pagingSources.add(it)
}
@@ -582,7 +592,6 @@
val pageEventLists: ArrayList<ArrayList<PageEvent<Int>>> = ArrayList()
val job = launch {
- @OptIn(ExperimentalCoroutinesApi::class)
fetcher.flow.collectIndexed { index, pagingData ->
pagingDataList.add(index, pagingData)
pageEventLists.add(index, ArrayList())
diff --git a/paging/common/src/test/kotlin/androidx/paging/PageKeyedDataSourceTest.kt b/paging/common/src/test/kotlin/androidx/paging/PageKeyedDataSourceTest.kt
index 44398e8..c94b1cf 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PageKeyedDataSourceTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PageKeyedDataSourceTest.kt
@@ -18,6 +18,9 @@
import androidx.testutils.TestDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineDispatcher
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@@ -86,16 +89,17 @@
}
}
+ @OptIn(ExperimentalCoroutinesApi::class)
@Test
fun loadFullVerify() {
// validate paging entire ItemDataSource results in full, correctly ordered data
- val testCoroutineScope = CoroutineScope(EmptyCoroutineContext)
-
+ val dispatcher = TestCoroutineDispatcher()
+ val testCoroutineScope = CoroutineScope(dispatcher)
@Suppress("DEPRECATION")
val pagedList = PagedList.Builder(ItemDataSource(), 100)
.setCoroutineScope(testCoroutineScope)
- .setNotifyDispatcher(mainThread)
- .setFetchDispatcher(DirectDispatcher)
+ .setNotifyDispatcher(dispatcher)
+ .setFetchDispatcher(dispatcher)
.build()
// validate initial load
@@ -105,7 +109,7 @@
for (i in 0..PAGE_MAP.keys.size) {
pagedList.loadAround(0)
pagedList.loadAround(pagedList.size - 1)
- drain()
+ dispatcher.advanceUntilIdle()
}
// validate full load
@@ -148,7 +152,7 @@
@Suppress("DEPRECATION")
PagedList.Builder(dataSource, 10)
.setNotifyDispatcher(FailDispatcher())
- .setFetchDispatcher(DirectDispatcher)
+ .setFetchDispatcher(Dispatchers.IO)
.build()
}
@@ -248,7 +252,7 @@
val pagedList = PagedList.Builder(dataSource, 10)
.setBoundaryCallback(boundaryCallback)
.setCoroutineScope(testCoroutineScope)
- .setFetchDispatcher(dispatcher)
+ .setFetchDispatcher(Dispatchers.Unconfined)
.setNotifyDispatcher(dispatcher)
.build()
@@ -301,7 +305,7 @@
val pagedList = PagedList.Builder(dataSource, 10)
.setBoundaryCallback(boundaryCallback)
.setCoroutineScope(testCoroutineScope)
- .setFetchDispatcher(dispatcher)
+ .setFetchDispatcher(Dispatchers.Unconfined)
.setNotifyDispatcher(dispatcher)
.build()
diff --git a/paging/common/src/test/kotlin/androidx/paging/PagedListTest.kt b/paging/common/src/test/kotlin/androidx/paging/PagedListTest.kt
index bd766f3..a0ea57f 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PagedListTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PagedListTest.kt
@@ -21,11 +21,14 @@
import androidx.testutils.TestDispatcher
import androidx.testutils.TestExecutor
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import java.util.concurrent.Executor
+import kotlin.concurrent.thread
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
@@ -53,10 +56,18 @@
@Test
fun createLegacy() {
+ val slowFetchExecutor = Executor {
+ // just be slow to ensure `build()` really waited on fetch to complete.
+ // but still run it on another thread to ensure we are not blocking the test here
+ thread {
+ Thread.sleep(1000)
+ it.run()
+ }
+ }
@Suppress("DEPRECATION")
val pagedList = PagedList.Builder(TestPositionalDataSource(ITEMS), 100)
.setNotifyExecutor(TestExecutor())
- .setFetchExecutor(TestExecutor())
+ .setFetchExecutor(slowFetchExecutor)
.build()
// if build succeeds without flushing an executor, success!
assertEquals(ITEMS, pagedList)
@@ -76,8 +87,8 @@
pagingSource,
null,
testCoroutineScope,
- DirectDispatcher,
- DirectDispatcher,
+ Dispatchers.Default,
+ Dispatchers.IO,
null,
Config(10),
0
@@ -104,8 +115,8 @@
pagingSource,
null,
testCoroutineScope,
- DirectDispatcher,
- DirectDispatcher,
+ Dispatchers.Default,
+ Dispatchers.IO,
null,
Config(10),
0
@@ -126,8 +137,8 @@
@Suppress("DEPRECATION")
val pagedList = PagedList.Builder(pagingSource, initialPage, config)
- .setNotifyDispatcher(DirectDispatcher)
- .setFetchDispatcher(DirectDispatcher)
+ .setNotifyDispatcher(Dispatchers.Default)
+ .setFetchDispatcher(Dispatchers.IO)
.build()
assertEquals(pagingSource, pagedList.pagingSource)
diff --git a/paging/common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt b/paging/common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt
index 6a6395c..883b4f2 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt
@@ -524,8 +524,12 @@
previousList: NullPaddedList<Int>,
newList: NullPaddedList<Int>,
newCombinedLoadStates: CombinedLoadStates,
- lastAccessedIndex: Int
- ): Int? = null
+ lastAccessedIndex: Int,
+ onListPresentable: () -> Unit
+ ): Int? {
+ onListPresentable()
+ return null
+ }
}
internal val dummyReceiver = object : UiReceiver {
diff --git a/paging/common/src/test/kotlin/androidx/paging/PagingSourceTest.kt b/paging/common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
index a5b05f9..6f90f6b 100644
--- a/paging/common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/PagingSourceTest.kt
@@ -256,7 +256,6 @@
private var error = false
- @OptIn(ExperimentalPagingApi::class)
override fun getRefreshKey(state: PagingState<Key, Item>): Key? {
return state.anchorPosition
?.let { anchorPosition -> state.closestItemToPosition(anchorPosition) }
diff --git a/paging/common/src/test/kotlin/androidx/paging/RemoteMediatorAccessorTest.kt b/paging/common/src/test/kotlin/androidx/paging/RemoteMediatorAccessorTest.kt
index 151a55d..29d1dbd 100644
--- a/paging/common/src/test/kotlin/androidx/paging/RemoteMediatorAccessorTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/RemoteMediatorAccessorTest.kt
@@ -39,6 +39,7 @@
class RemoteMediatorAccessorTest {
private val testScope = TestCoroutineScope()
private var mockStateId = 0
+
// creates a unique state using the anchor position to be able to do equals check in assertions
private fun createMockState(
anchorPosition: Int? = mockStateId++
@@ -52,6 +53,110 @@
}
@Test
+ fun load_reportsPrependLoadState() = testScope.runBlockingTest {
+ val emptyState = PagingState<Int, Int>(listOf(), null, PagingConfig(10), COUNT_UNDEFINED)
+ val remoteMediator = RemoteMediatorMock(loadDelay = 1000)
+ val remoteMediatorAccessor = createAccessor(remoteMediator)
+
+ // Assert initial state is NotLoading.Incomplete.
+ assertEquals(
+ LoadStates.IDLE.copy(prepend = LoadState.NotLoading.Incomplete),
+ remoteMediatorAccessor.state.value,
+ )
+
+ // Start a PREPEND load.
+ remoteMediatorAccessor.requestLoad(
+ loadType = PREPEND,
+ pagingState = emptyState,
+ )
+
+ // Assert state is immediately set to Loading.
+ assertEquals(
+ LoadStates.IDLE.copy(prepend = LoadState.Loading),
+ remoteMediatorAccessor.state.value,
+ )
+
+ // Wait for load to finish.
+ advanceUntilIdle()
+
+ // Assert state is set to NotLoading.Incomplete.
+ assertEquals(
+ LoadStates.IDLE.copy(prepend = LoadState.NotLoading.Incomplete),
+ remoteMediatorAccessor.state.value,
+ )
+
+ // Start a PREPEND load which results in endOfPaginationReached = true.
+ remoteMediator.loadCallback = { _, _ ->
+ RemoteMediator.MediatorResult.Success(endOfPaginationReached = true)
+ }
+ remoteMediatorAccessor.requestLoad(
+ loadType = PREPEND,
+ pagingState = emptyState,
+ )
+
+ // Wait for load to finish.
+ advanceUntilIdle()
+
+ // Assert state is set to NotLoading.Incomplete.
+ assertEquals(
+ LoadStates.IDLE.copy(prepend = LoadState.NotLoading.Complete),
+ remoteMediatorAccessor.state.value,
+ )
+ }
+
+ @Test
+ fun load_reportsAppendLoadState() = testScope.runBlockingTest {
+ val emptyState = PagingState<Int, Int>(listOf(), null, PagingConfig(10), COUNT_UNDEFINED)
+ val remoteMediator = RemoteMediatorMock(loadDelay = 1000)
+ val remoteMediatorAccessor = createAccessor(remoteMediator)
+
+ // Assert initial state is NotLoading.Incomplete.
+ assertEquals(
+ LoadStates.IDLE.copy(prepend = LoadState.NotLoading.Incomplete),
+ remoteMediatorAccessor.state.value,
+ )
+
+ // Start a APPEND load.
+ remoteMediatorAccessor.requestLoad(
+ loadType = APPEND,
+ pagingState = emptyState,
+ )
+
+ // Assert state is immediately set to Loading.
+ assertEquals(
+ LoadStates.IDLE.copy(append = LoadState.Loading),
+ remoteMediatorAccessor.state.value,
+ )
+
+ // Wait for load to finish.
+ advanceUntilIdle()
+
+ // Assert state is set to NotLoading.Incomplete.
+ assertEquals(
+ LoadStates.IDLE.copy(append = LoadState.NotLoading.Incomplete),
+ remoteMediatorAccessor.state.value,
+ )
+
+ // Start a APPEND load which results in endOfPaginationReached = true.
+ remoteMediator.loadCallback = { _, _ ->
+ RemoteMediator.MediatorResult.Success(endOfPaginationReached = true)
+ }
+ remoteMediatorAccessor.requestLoad(
+ loadType = APPEND,
+ pagingState = emptyState,
+ )
+
+ // Wait for load to finish.
+ advanceUntilIdle()
+
+ // Assert state is set to NotLoading.Incomplete.
+ assertEquals(
+ LoadStates.IDLE.copy(append = LoadState.NotLoading.Complete),
+ remoteMediatorAccessor.state.value,
+ )
+ }
+
+ @Test
fun load_conflatesPrepend() = testScope.runBlockingTest {
val remoteMediator = RemoteMediatorMock(loadDelay = 1000)
val remoteMediatorAccessor = createAccessor(remoteMediator)
diff --git a/paging/common/src/test/kotlin/androidx/paging/SeparatorsTest.kt b/paging/common/src/test/kotlin/androidx/paging/SeparatorsTest.kt
index 38d8dc7..276285b 100644
--- a/paging/common/src/test/kotlin/androidx/paging/SeparatorsTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/SeparatorsTest.kt
@@ -19,10 +19,12 @@
import androidx.paging.LoadState.NotLoading
import androidx.paging.LoadType.APPEND
import androidx.paging.LoadType.PREPEND
+import androidx.paging.LoadType.REFRESH
import androidx.paging.PageEvent.Drop
import androidx.paging.PageEvent.Insert.Companion.Append
import androidx.paging.PageEvent.Insert.Companion.Prepend
import androidx.paging.PageEvent.Insert.Companion.Refresh
+import androidx.paging.PageEvent.LoadStateUpdate
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
@@ -985,6 +987,481 @@
)
}
+ @Test
+ fun remoteRefreshEndOfPaginationReached() = runBlockingTest {
+ assertThat(
+ flowOf(
+ LoadStateUpdate(
+ loadType = REFRESH,
+ fromMediator = true,
+ loadState = LoadState.Loading
+ ),
+ LoadStateUpdate(
+ loadType = REFRESH,
+ fromMediator = false,
+ loadState = LoadState.Loading
+ ),
+ Refresh(
+ pages = listOf(listOf("a1")).toTransformablePages(),
+ placeholdersBefore = 1,
+ placeholdersAfter = 1,
+ combinedLoadStates = remoteLoadStatesOf(
+ appendLocal = NotLoading.Complete,
+ prependLocal = NotLoading.Complete,
+ )
+ ),
+ LoadStateUpdate(
+ loadType = REFRESH,
+ fromMediator = true,
+ loadState = NotLoading(endOfPaginationReached = true)
+ ),
+ LoadStateUpdate(
+ loadType = PREPEND,
+ fromMediator = true,
+ loadState = NotLoading(endOfPaginationReached = true)
+ ),
+ LoadStateUpdate(
+ loadType = APPEND,
+ fromMediator = true,
+ loadState = NotLoading(endOfPaginationReached = true)
+ ),
+ ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+ ).isEqualTo(
+ listOf(
+ LoadStateUpdate(
+ loadType = REFRESH,
+ fromMediator = true,
+ loadState = LoadState.Loading
+ ),
+ LoadStateUpdate(
+ loadType = REFRESH,
+ fromMediator = false,
+ loadState = LoadState.Loading
+ ),
+ Refresh(
+ pages = listOf(listOf("a1")).toTransformablePages(),
+ placeholdersBefore = 1,
+ placeholdersAfter = 1,
+ combinedLoadStates = remoteLoadStatesOf(
+ appendLocal = NotLoading.Complete,
+ prependLocal = NotLoading.Complete,
+ )
+ ),
+ LoadStateUpdate(
+ loadType = REFRESH,
+ fromMediator = true,
+ loadState = NotLoading(endOfPaginationReached = true)
+ ),
+ Prepend(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffsets = intArrayOf(0),
+ data = listOf("A"),
+ hintOriginalIndices = listOf(0),
+ hintOriginalPageOffset = 0,
+ ),
+ ),
+ placeholdersBefore = 1,
+ combinedLoadStates = remoteLoadStatesOf(
+ refresh = NotLoading.Complete,
+ prepend = NotLoading.Complete,
+ appendLocal = NotLoading.Complete,
+ prependLocal = NotLoading.Complete,
+ refreshRemote = NotLoading.Complete,
+ prependRemote = NotLoading.Complete,
+ ),
+ ),
+ Append(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffsets = intArrayOf(0),
+ data = listOf("END"),
+ hintOriginalIndices = listOf(0),
+ hintOriginalPageOffset = 0,
+ ),
+ ),
+ placeholdersAfter = 1,
+ combinedLoadStates = remoteLoadStatesOf(
+ refresh = NotLoading.Complete,
+ prepend = NotLoading.Complete,
+ append = NotLoading.Complete,
+ appendLocal = NotLoading.Complete,
+ prependLocal = NotLoading.Complete,
+ refreshRemote = NotLoading.Complete,
+ prependRemote = NotLoading.Complete,
+ appendRemote = NotLoading.Complete,
+ ),
+ ),
+ )
+ )
+ }
+
+ @Test
+ fun remotePrependEndOfPaginationReached() = runBlockingTest {
+ assertThat(
+ flowOf(
+ Refresh(
+ pages = listOf(listOf("a1")).toTransformablePages(),
+ placeholdersBefore = 1,
+ placeholdersAfter = 1,
+ combinedLoadStates = remoteLoadStatesOf(
+ appendLocal = NotLoading.Complete,
+ prependLocal = NotLoading.Complete,
+ )
+ ),
+ // Signalling that remote prepend is done triggers the header to resolve.
+ LoadStateUpdate(PREPEND, true, NotLoading.Complete),
+ ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+ ).isEqualTo(
+ listOf(
+ Refresh(
+ pages = listOf(listOf("a1")).toTransformablePages(),
+ placeholdersBefore = 1,
+ placeholdersAfter = 1,
+ combinedLoadStates = remoteLoadStatesOf(
+ appendLocal = NotLoading.Complete,
+ prependLocal = NotLoading.Complete,
+ )
+ ),
+ Prepend(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffsets = intArrayOf(0),
+ data = listOf("A"),
+ hintOriginalIndices = listOf(0),
+ hintOriginalPageOffset = 0,
+ ),
+ ),
+ placeholdersBefore = 1,
+ combinedLoadStates = remoteLoadStatesOf(
+ prepend = NotLoading.Complete,
+ appendLocal = NotLoading.Complete,
+ prependLocal = NotLoading.Complete,
+ prependRemote = NotLoading.Complete,
+ ),
+ ),
+ )
+ )
+ }
+
+ @Test
+ fun remotePrependEndOfPaginationReachedWithDrops() = runBlockingTest {
+ assertThat(
+ flowOf(
+ Refresh(
+ pages = listOf(listOf("b1")).toTransformablePages(),
+ placeholdersBefore = 1,
+ placeholdersAfter = 1,
+ combinedLoadStates = remoteLoadStatesOf(
+ appendLocal = NotLoading.Complete,
+ prependLocal = NotLoading.Incomplete,
+ )
+ ),
+ Prepend(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffset = -1,
+ data = listOf("a1"),
+ )
+ ),
+ placeholdersBefore = 0,
+ combinedLoadStates = remoteLoadStatesOf(
+ appendLocal = NotLoading.Complete,
+ prependLocal = NotLoading.Complete,
+ )
+ ),
+ // Signalling that remote prepend is done triggers the header to resolve.
+ LoadStateUpdate(PREPEND, true, NotLoading.Complete),
+ // Drop the first page, header and separator between "b1" and "a1"
+ Drop(
+ loadType = PREPEND,
+ minPageOffset = -1,
+ maxPageOffset = -1,
+ placeholdersRemaining = 1
+ ),
+ Prepend(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffset = -1,
+ data = listOf("a1"),
+ )
+ ),
+ placeholdersBefore = 0,
+ combinedLoadStates = remoteLoadStatesOf(
+ prepend = NotLoading.Complete,
+ appendLocal = NotLoading.Complete,
+ prependLocal = NotLoading.Complete,
+ prependRemote = NotLoading.Complete,
+ )
+ ),
+ ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+ ).isEqualTo(
+ listOf(
+ Refresh(
+ pages = listOf(listOf("b1")).toTransformablePages(),
+ placeholdersBefore = 1,
+ placeholdersAfter = 1,
+ combinedLoadStates = remoteLoadStatesOf(
+ appendLocal = NotLoading.Complete,
+ prependLocal = NotLoading.Incomplete,
+ )
+ ),
+ Prepend(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffset = -1,
+ data = listOf("a1"),
+ ),
+ TransformablePage(
+ originalPageOffsets = intArrayOf(-1, 0),
+ data = listOf("B"),
+ hintOriginalIndices = listOf(0),
+ hintOriginalPageOffset = -1
+ ),
+ ),
+ placeholdersBefore = 0,
+ combinedLoadStates = remoteLoadStatesOf(
+ appendLocal = NotLoading.Complete,
+ prependLocal = NotLoading.Complete,
+ )
+ ),
+ Prepend(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffsets = intArrayOf(-1),
+ data = listOf("A"),
+ hintOriginalIndices = listOf(0),
+ hintOriginalPageOffset = -1,
+ ),
+ ),
+ placeholdersBefore = 0,
+ combinedLoadStates = remoteLoadStatesOf(
+ prepend = NotLoading.Complete,
+ appendLocal = NotLoading.Complete,
+ prependLocal = NotLoading.Complete,
+ prependRemote = NotLoading.Complete,
+ ),
+ ),
+ Drop(
+ loadType = PREPEND,
+ minPageOffset = -1,
+ maxPageOffset = -1,
+ placeholdersRemaining = 1
+ ),
+ Prepend(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffsets = intArrayOf(-1),
+ data = listOf("A"),
+ hintOriginalIndices = listOf(0),
+ hintOriginalPageOffset = -1,
+ ),
+ TransformablePage(
+ originalPageOffset = -1,
+ data = listOf("a1"),
+ ),
+ TransformablePage(
+ originalPageOffsets = intArrayOf(-1, 0),
+ data = listOf("B"),
+ hintOriginalIndices = listOf(0),
+ hintOriginalPageOffset = -1
+ ),
+ ),
+ placeholdersBefore = 0,
+ combinedLoadStates = remoteLoadStatesOf(
+ prepend = NotLoading.Complete,
+ appendLocal = NotLoading.Complete,
+ prependLocal = NotLoading.Complete,
+ prependRemote = NotLoading.Complete,
+ )
+ ),
+ )
+ )
+ }
+
+ @Test
+ fun remoteAppendEndOfPaginationReached() = runBlockingTest {
+ assertThat(
+ flowOf(
+ Refresh(
+ pages = listOf(listOf("a1")).toTransformablePages(),
+ placeholdersBefore = 1,
+ placeholdersAfter = 1,
+ combinedLoadStates = remoteLoadStatesOf(
+ appendLocal = NotLoading.Complete,
+ prependLocal = NotLoading.Complete,
+ )
+ ),
+ // Signalling that remote append is done triggers the footer to resolve.
+ LoadStateUpdate(APPEND, true, NotLoading.Complete),
+ ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+ ).isEqualTo(
+ listOf(
+ Refresh(
+ pages = listOf(listOf("a1")).toTransformablePages(),
+ placeholdersBefore = 1,
+ placeholdersAfter = 1,
+ combinedLoadStates = remoteLoadStatesOf(
+ appendLocal = NotLoading.Complete,
+ prependLocal = NotLoading.Complete,
+ )
+ ),
+ Append(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffsets = intArrayOf(0),
+ data = listOf("END"),
+ hintOriginalIndices = listOf(0),
+ hintOriginalPageOffset = 0,
+ ),
+ ),
+ placeholdersAfter = 1,
+ combinedLoadStates = remoteLoadStatesOf(
+ append = NotLoading.Complete,
+ appendLocal = NotLoading.Complete,
+ prependLocal = NotLoading.Complete,
+ appendRemote = NotLoading.Complete,
+ ),
+ ),
+ )
+ )
+ }
+
+ @Test
+ fun remoteAppendEndOfPaginationReachedWithDrops() = runBlockingTest {
+ assertThat(
+ flowOf(
+ Refresh(
+ pages = listOf(listOf("b1")).toTransformablePages(),
+ placeholdersBefore = 1,
+ placeholdersAfter = 1,
+ combinedLoadStates = remoteLoadStatesOf(
+ prependLocal = NotLoading.Complete,
+ appendLocal = NotLoading.Incomplete,
+ )
+ ),
+ Append(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffset = 1,
+ data = listOf("c1"),
+ )
+ ),
+ placeholdersAfter = 0,
+ combinedLoadStates = remoteLoadStatesOf(
+ prependLocal = NotLoading.Complete,
+ appendLocal = NotLoading.Complete,
+ )
+ ),
+ // Signalling that remote append is done triggers the footer to resolve.
+ LoadStateUpdate(APPEND, true, NotLoading.Complete),
+ // Drop the last page, footer and separator between "b1" and "c1"
+ Drop(
+ loadType = APPEND,
+ minPageOffset = 1,
+ maxPageOffset = 1,
+ placeholdersRemaining = 1
+ ),
+ Append(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffset = 1,
+ data = listOf("c1"),
+ )
+ ),
+ placeholdersAfter = 0,
+ combinedLoadStates = remoteLoadStatesOf(
+ append = NotLoading.Complete,
+ prependLocal = NotLoading.Complete,
+ appendLocal = NotLoading.Complete,
+ appendRemote = NotLoading.Complete,
+ )
+ ),
+ ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+ ).isEqualTo(
+ listOf(
+ Refresh(
+ pages = listOf(listOf("b1")).toTransformablePages(),
+ placeholdersBefore = 1,
+ placeholdersAfter = 1,
+ combinedLoadStates = remoteLoadStatesOf(
+ prependLocal = NotLoading.Complete,
+ appendLocal = NotLoading.Incomplete,
+ )
+ ),
+ Append(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffsets = intArrayOf(0, 1),
+ data = listOf("C"),
+ hintOriginalIndices = listOf(0),
+ hintOriginalPageOffset = 1
+ ),
+ TransformablePage(
+ originalPageOffset = 1,
+ data = listOf("c1"),
+ ),
+ ),
+ placeholdersAfter = 0,
+ combinedLoadStates = remoteLoadStatesOf(
+ prependLocal = NotLoading.Complete,
+ appendLocal = NotLoading.Complete,
+ )
+ ),
+ Append(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffsets = intArrayOf(1),
+ data = listOf("END"),
+ hintOriginalIndices = listOf(0),
+ hintOriginalPageOffset = 1,
+ ),
+ ),
+ placeholdersAfter = 0,
+ combinedLoadStates = remoteLoadStatesOf(
+ append = NotLoading.Complete,
+ prependLocal = NotLoading.Complete,
+ appendLocal = NotLoading.Complete,
+ appendRemote = NotLoading.Complete,
+ ),
+ ),
+ Drop(
+ loadType = APPEND,
+ minPageOffset = 1,
+ maxPageOffset = 1,
+ placeholdersRemaining = 1
+ ),
+ Append(
+ pages = listOf(
+ TransformablePage(
+ originalPageOffsets = intArrayOf(0, 1),
+ data = listOf("C"),
+ hintOriginalIndices = listOf(0),
+ hintOriginalPageOffset = 1
+ ),
+ TransformablePage(
+ originalPageOffset = 1,
+ data = listOf("c1"),
+ ),
+ TransformablePage(
+ originalPageOffsets = intArrayOf(1),
+ data = listOf("END"),
+ hintOriginalIndices = listOf(0),
+ hintOriginalPageOffset = 1
+ ),
+ ),
+ placeholdersAfter = 0,
+ combinedLoadStates = remoteLoadStatesOf(
+ append = NotLoading.Complete,
+ prependLocal = NotLoading.Complete,
+ appendLocal = NotLoading.Complete,
+ appendRemote = NotLoading.Complete,
+ )
+ ),
+ )
+ )
+ }
+
companion object {
/**
* Creates an upper-case letter at the beginning of each section of strings that start
diff --git a/paging/common/src/test/kotlin/androidx/paging/WrappedItemKeyedDataSourceTest.kt b/paging/common/src/test/kotlin/androidx/paging/WrappedItemKeyedDataSourceTest.kt
index 2c90329..f642d6c 100644
--- a/paging/common/src/test/kotlin/androidx/paging/WrappedItemKeyedDataSourceTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/WrappedItemKeyedDataSourceTest.kt
@@ -21,7 +21,6 @@
import org.junit.runners.JUnit4
import kotlin.test.assertTrue
-@OptIn(ExperimentalPagingApi::class)
@RunWith(JUnit4::class)
class WrappedItemKeyedDataSourceTest {
diff --git a/paging/common/src/test/kotlin/androidx/paging/WrappedPageKeyedDataSourceTest.kt b/paging/common/src/test/kotlin/androidx/paging/WrappedPageKeyedDataSourceTest.kt
index 129cd3f..cf4ef24 100644
--- a/paging/common/src/test/kotlin/androidx/paging/WrappedPageKeyedDataSourceTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/WrappedPageKeyedDataSourceTest.kt
@@ -21,7 +21,6 @@
import org.junit.runners.JUnit4
import kotlin.test.assertTrue
-@OptIn(ExperimentalPagingApi::class)
@RunWith(JUnit4::class)
class WrappedPageKeyedDataSourceTest {
diff --git a/paging/common/src/test/kotlin/androidx/paging/WrappedPositionalDataSourceTest.kt b/paging/common/src/test/kotlin/androidx/paging/WrappedPositionalDataSourceTest.kt
index cc02f84..b56fe139 100644
--- a/paging/common/src/test/kotlin/androidx/paging/WrappedPositionalDataSourceTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/WrappedPositionalDataSourceTest.kt
@@ -21,7 +21,6 @@
import org.junit.runners.JUnit4
import kotlin.test.assertTrue
-@OptIn(ExperimentalPagingApi::class)
@RunWith(JUnit4::class)
class WrappedPositionalDataSourceTest {
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/custom/ItemDataSource.kt b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/custom/ItemDataSource.kt
index 5ce84fd..a107d6e 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/custom/ItemDataSource.kt
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/custom/ItemDataSource.kt
@@ -18,7 +18,6 @@
import android.graphics.Color
import androidx.annotation.ColorInt
-import androidx.paging.ExperimentalPagingApi
import androidx.paging.PagingSource
import androidx.paging.PagingState
import kotlinx.coroutines.delay
@@ -34,7 +33,6 @@
private val generationId = sGenerationId++
- @OptIn(ExperimentalPagingApi::class)
override fun getRefreshKey(state: PagingState<Int, Item>): Int? = state.anchorPosition
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Item> =
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/room/CustomerDao.java b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/room/CustomerDao.java
index 1b400ca..58cd34c 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/room/CustomerDao.java
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/room/CustomerDao.java
@@ -52,16 +52,16 @@
void removeAll();
/**
- * @return DataSource.Factory of customers, ordered by last name. Use
+ * @return DataSource.Factory of customers, ordered by id. Use
* {@link androidx.paging.LivePagedListBuilder} to get a LiveData of PagedLists.
*/
- @Query("SELECT * FROM customer ORDER BY mLastName ASC")
+ @Query("SELECT * FROM customer ORDER BY mId ASC")
DataSource.Factory<Integer, Customer> loadPagedAgeOrder();
/**
- * @return PagingSource of customers, ordered by last name.
+ * @return PagingSource of customers, ordered by id.
*/
- @Query("SELECT * FROM customer ORDER BY mLastName ASC")
+ @Query("SELECT * FROM customer ORDER BY mId ASC")
PagingSource<Integer, Customer> loadPagedAgeOrderPagingSource();
/**
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3/ItemPagingSource.kt b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3/ItemPagingSource.kt
index a8eb290..e7f4e4a 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3/ItemPagingSource.kt
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3/ItemPagingSource.kt
@@ -18,7 +18,6 @@
import android.graphics.Color
import androidx.annotation.ColorInt
-import androidx.paging.ExperimentalPagingApi
import androidx.paging.PagingSource
import androidx.paging.PagingState
import kotlinx.coroutines.delay
@@ -34,7 +33,6 @@
private val generationId = sGenerationId++
- @OptIn(ExperimentalPagingApi::class)
override fun getRefreshKey(state: PagingState<Int, Item>): Int? = state.anchorPosition
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Item> =
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3/V3Activity.kt b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3/V3Activity.kt
index bfd595f..a8288e4 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3/V3Activity.kt
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3/V3Activity.kt
@@ -30,7 +30,6 @@
import androidx.paging.integration.testapp.R
import androidx.paging.map
import androidx.recyclerview.widget.RecyclerView
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@@ -50,7 +49,6 @@
}
// NOTE: lifecycleScope means we don't respect paused state here
lifecycleScope.launch {
- @OptIn(ExperimentalCoroutinesApi::class)
viewModel.flow
.map { pagingData ->
pagingData.map { it.copy(text = "${it.text} - $orientationText") }
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/NetworkCustomerPagingSource.kt b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/NetworkCustomerPagingSource.kt
index ae1744b..13709de 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/NetworkCustomerPagingSource.kt
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/NetworkCustomerPagingSource.kt
@@ -16,7 +16,6 @@
package androidx.paging.integration.testapp.v3room
-import androidx.paging.ExperimentalPagingApi
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.paging.integration.testapp.room.Customer
@@ -28,20 +27,27 @@
internal class NetworkCustomerPagingSource : PagingSource<Int, Customer>() {
private fun createCustomer(i: Int): Customer {
val customer = Customer()
- customer.name = UUID.randomUUID().toString()
+ customer.name = "customer_$i"
customer.lastName = "${"%04d".format(i)}_${UUID.randomUUID()}"
return customer
}
- @OptIn(ExperimentalPagingApi::class)
- override fun getRefreshKey(state: PagingState<Int, Customer>): Int? = state.anchorPosition
+ override fun getRefreshKey(
+ state: PagingState<Int, Customer>
+ ): Int? = state.anchorPosition?.let {
+ maxOf(0, it - 5)
+ }
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Customer> {
val key = params.key ?: 0
- val data = List(params.loadSize) { createCustomer(it + key) }
+ val data = if (params is LoadParams.Prepend) {
+ List(params.loadSize) { createCustomer(it + key - params.loadSize) }
+ } else {
+ List(params.loadSize) { createCustomer(it + key) }
+ }
return LoadResult.Page(
data = data,
- prevKey = if (key > 0) key - 1 else null,
+ prevKey = if (key > 0) key else null,
nextKey = key + data.size
)
}
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 7ed4007..761a3d8 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
@@ -36,7 +36,9 @@
loadType: LoadType,
state: PagingState<Int, Customer>
): MediatorResult {
- if (loadType == LoadType.PREPEND) return MediatorResult.Success(false)
+ if (loadType == LoadType.PREPEND) {
+ return MediatorResult.Success(endOfPaginationReached = true)
+ }
// TODO: Move this to be a more fully featured sample which demonstrated key translation
// between two types of PagingSources where the keys do not map 1:1.
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomActivity.kt b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomActivity.kt
index 454b2d2..8bad4dc 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomActivity.kt
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomActivity.kt
@@ -23,7 +23,6 @@
import androidx.lifecycle.lifecycleScope
import androidx.paging.integration.testapp.R
import androidx.recyclerview.widget.RecyclerView
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@@ -39,7 +38,6 @@
recyclerView.adapter = adapter
lifecycleScope.launch {
- @OptIn(ExperimentalCoroutinesApi::class)
viewModel.flow.collectLatest {
adapter.submitData(it)
}
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomAdapter.kt b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomAdapter.kt
index 5e0f1bf..a1e2dfb 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomAdapter.kt
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomAdapter.kt
@@ -40,7 +40,7 @@
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
- (holder.itemView as TextView).text = item.lastName
+ (holder.itemView as TextView).text = item.name
holder.itemView.setBackgroundColor(Color.BLUE)
} else {
(holder.itemView as TextView).setText(R.string.loading)
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 27e4eea..8c84a91 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
@@ -21,6 +21,7 @@
import androidx.arch.core.executor.ArchTaskExecutor
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
+import androidx.paging.ExperimentalPagingApi
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.cachedIn
@@ -57,6 +58,7 @@
.executeOnDiskIO { database.customerDao.removeAll() }
}
+ @OptIn(ExperimentalPagingApi::class)
val flow = Pager(
PagingConfig(10),
remoteMediator = V3RemoteMediator(
@@ -76,7 +78,7 @@
Customer().apply {
id = -1
name = "RIGHT ABOVE DIVIDER"
- lastName = "LAST NAME"
+ lastName = "RIGHT ABOVE DIVIDER"
}
}
}
@@ -85,19 +87,21 @@
Customer().apply {
id = -2
name = "RIGHT BELOW DIVIDER"
- lastName = "LAST NAME"
+ lastName = "RIGHT BELOW DIVIDER"
}
} else null
}
.insertHeaderItem(
Customer().apply {
id = Int.MIN_VALUE
+ name = "HEADER"
lastName = "HEADER"
}
)
.insertFooterItem(
Customer().apply {
id = Int.MAX_VALUE
+ name = "FOOTER"
lastName = "FOOTER"
}
)
diff --git a/paging/paging-compose/build.gradle b/paging/paging-compose/build.gradle
index 2d099c2..ef5d031 100644
--- a/paging/paging-compose/build.gradle
+++ b/paging/paging-compose/build.gradle
@@ -35,7 +35,7 @@
implementation(KOTLIN_STDLIB)
api projectOrArtifact(":compose:foundation:foundation")
- api("androidx.paging:paging-common-ktx:3.0.0-alpha06")
+ api projectOrArtifact(":paging:paging-common")
androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
androidTestImplementation projectOrArtifact(':internal-testutils-paging')
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 12200c8..f6b3ac0 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
@@ -89,8 +89,10 @@
previousList: NullPaddedList<T>,
newList: NullPaddedList<T>,
newCombinedLoadStates: CombinedLoadStates,
- lastAccessedIndex: Int
+ lastAccessedIndex: Int,
+ onListPresentable: () -> Unit,
): Int? {
+ onListPresentable()
// TODO: This logic may be changed after the implementation of an async model which
// composes the offscreen elements
recomposerPlaceholder.value++
@@ -166,7 +168,12 @@
* A [CombinedLoadStates] object which represents the current loading state.
*/
public var loadState: CombinedLoadStates by mutableStateOf(
- CombinedLoadStates(InitialLoadStates)
+ CombinedLoadStates(
+ refresh = InitialLoadStates.refresh,
+ prepend = InitialLoadStates.prepend,
+ append = InitialLoadStates.append,
+ source = InitialLoadStates,
+ )
)
private set
diff --git a/paging/runtime/build.gradle b/paging/runtime/build.gradle
index 9a80921..4c3019b 100644
--- a/paging/runtime/build.gradle
+++ b/paging/runtime/build.gradle
@@ -26,6 +26,12 @@
id("kotlin-android")
}
+android {
+ defaultConfig {
+ multiDexEnabled true
+ }
+}
+
dependencies {
api(project(":paging:paging-common"))
// Ensure that the -ktx dependency graph mirrors the Java dependency graph
@@ -46,9 +52,11 @@
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
androidTestImplementation(ANDROIDX_TEST_RUNNER)
androidTestImplementation("androidx.arch.core:core-testing:2.1.0")
+ androidTestImplementation(TRUTH)
androidTestImplementation(KOTLIN_TEST)
androidTestImplementation(KOTLIN_COROUTINES_TEST)
androidTestImplementation(JUNIT)
+ androidTestImplementation(MULTIDEX)
}
androidx {
diff --git a/paging/runtime/src/androidTest/java/androidx/paging/AsyncPagedListDifferTest.kt b/paging/runtime/src/androidTest/java/androidx/paging/AsyncPagedListDifferTest.kt
index 0435993..b0e3005 100644
--- a/paging/runtime/src/androidTest/java/androidx/paging/AsyncPagedListDifferTest.kt
+++ b/paging/runtime/src/androidTest/java/androidx/paging/AsyncPagedListDifferTest.kt
@@ -65,11 +65,16 @@
data: List<V>,
initialKey: Int
): PagedList<V> {
+ // unblock page loading thread to allow build to succeed
+ pageLoadingThread.autoRun = true
return PagedList.Builder(TestPositionalDataSource(data), config)
.setInitialKey(initialKey)
.setNotifyExecutor(mainThread)
.setFetchExecutor(pageLoadingThread)
.build()
+ .also {
+ pageLoadingThread.autoRun = false
+ }
}
@Test
diff --git a/paging/runtime/src/androidTest/java/androidx/paging/AsyncPagingDataDifferTest.kt b/paging/runtime/src/androidTest/java/androidx/paging/AsyncPagingDataDifferTest.kt
index 6bb8c66..6f89fd1 100644
--- a/paging/runtime/src/androidTest/java/androidx/paging/AsyncPagingDataDifferTest.kt
+++ b/paging/runtime/src/androidTest/java/androidx/paging/AsyncPagingDataDifferTest.kt
@@ -23,7 +23,6 @@
import androidx.paging.ListUpdateEvent.Removed
import androidx.paging.LoadState.Loading
import androidx.paging.LoadState.NotLoading
-import androidx.paging.LoadType.REFRESH
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -133,9 +132,10 @@
// empty previous list.
assertEvents(
listOf(
- REFRESH to Loading,
- REFRESH to NotLoading(endOfPaginationReached = false)
- ).toCombinedLoadStatesLocal(),
+ localLoadStatesOf(),
+ localLoadStatesOf(refreshLocal = Loading),
+ localLoadStatesOf(refreshLocal = NotLoading(endOfPaginationReached = false)),
+ ),
loadEvents
)
loadEvents.clear()
@@ -152,8 +152,8 @@
localLoadStatesOf(
refreshLocal = NotLoading(endOfPaginationReached = false),
prependLocal = NotLoading(endOfPaginationReached = true),
- appendLocal = NotLoading(endOfPaginationReached = true)
- )
+ appendLocal = NotLoading(endOfPaginationReached = true),
+ ),
),
actual = loadEvents
)
@@ -190,9 +190,10 @@
// empty previous list.
assertEvents(
listOf(
- REFRESH to Loading,
- REFRESH to NotLoading(endOfPaginationReached = false)
- ).toCombinedLoadStatesLocal(),
+ localLoadStatesOf(),
+ localLoadStatesOf(refreshLocal = Loading),
+ localLoadStatesOf(refreshLocal = NotLoading(endOfPaginationReached = false)),
+ ),
loadEvents
)
loadEvents.clear()
@@ -209,8 +210,8 @@
localLoadStatesOf(
refreshLocal = NotLoading(endOfPaginationReached = false),
prependLocal = NotLoading(endOfPaginationReached = true),
- appendLocal = NotLoading(endOfPaginationReached = true)
- )
+ appendLocal = NotLoading(endOfPaginationReached = true),
+ ),
),
actual = loadEvents
)
@@ -510,48 +511,21 @@
// Initial refresh
advanceUntilIdle()
- assertEquals(
- CombinedLoadStates(
- source = LoadStates(
- refresh = NotLoading(endOfPaginationReached = false),
- prepend = NotLoading(endOfPaginationReached = false),
- append = NotLoading(endOfPaginationReached = false)
- )
- ),
- combinedLoadStates
- )
+ assertEquals(localLoadStatesOf(), combinedLoadStates)
assertEquals(10, itemCount)
assertEquals(10, differ.itemCount)
// Append
differ.getItem(9)
advanceUntilIdle()
- assertEquals(
- CombinedLoadStates(
- source = LoadStates(
- refresh = NotLoading(endOfPaginationReached = false),
- prepend = NotLoading(endOfPaginationReached = false),
- append = NotLoading(endOfPaginationReached = false)
- )
- ),
- combinedLoadStates
- )
+ assertEquals(localLoadStatesOf(), combinedLoadStates)
assertEquals(20, itemCount)
assertEquals(20, differ.itemCount)
// Prepend
differ.getItem(0)
advanceUntilIdle()
- assertEquals(
- CombinedLoadStates(
- source = LoadStates(
- refresh = NotLoading(endOfPaginationReached = false),
- prepend = NotLoading(endOfPaginationReached = false),
- append = NotLoading(endOfPaginationReached = false)
- )
- ),
- combinedLoadStates
- )
+ assertEquals(localLoadStatesOf(), combinedLoadStates)
assertEquals(30, itemCount)
assertEquals(30, differ.itemCount)
@@ -584,52 +558,90 @@
// Initial refresh
advanceUntilIdle()
- assertEquals(
- CombinedLoadStates(
- source = LoadStates(
- refresh = NotLoading(endOfPaginationReached = false),
- prepend = NotLoading(endOfPaginationReached = false),
- append = NotLoading(endOfPaginationReached = false)
- )
- ),
- combinedLoadStates
- )
+ assertEquals(localLoadStatesOf(), combinedLoadStates)
assertEquals(10, itemCount)
assertEquals(10, differ.itemCount)
// Append
differ.getItem(9)
advanceUntilIdle()
- assertEquals(
- CombinedLoadStates(
- source = LoadStates(
- refresh = NotLoading(endOfPaginationReached = false),
- prepend = NotLoading(endOfPaginationReached = false),
- append = NotLoading(endOfPaginationReached = false)
- )
- ),
- combinedLoadStates
- )
+ assertEquals(localLoadStatesOf(), combinedLoadStates)
assertEquals(20, itemCount)
assertEquals(20, differ.itemCount)
// Prepend
differ.getItem(0)
advanceUntilIdle()
- assertEquals(
- CombinedLoadStates(
- source = LoadStates(
- refresh = NotLoading(endOfPaginationReached = false),
- prepend = NotLoading(endOfPaginationReached = false),
- append = NotLoading(endOfPaginationReached = false)
- )
- ),
- combinedLoadStates
- )
+ assertEquals(localLoadStatesOf(), combinedLoadStates)
assertEquals(30, itemCount)
assertEquals(30, differ.itemCount)
job.cancel()
}
}
+
+ @Test
+ fun listUpdateCallbackSynchronouslyUpdates() = testScope.runBlockingTest {
+ pauseDispatcher {
+ // Keep track of .snapshot() result within each ListUpdateCallback
+ val initialSnapshot: ItemSnapshotList<Int> = ItemSnapshotList(0, 0, emptyList())
+ var onInsertedSnapshot = initialSnapshot
+ var onRemovedSnapshot = initialSnapshot
+
+ val listUpdateCallback = object : ListUpdateCallback {
+ lateinit var differ: AsyncPagingDataDiffer<Int>
+
+ override fun onChanged(position: Int, count: Int, payload: Any?) {
+ // TODO: Trigger this callback so we can assert state at this point as well
+ }
+
+ override fun onMoved(fromPosition: Int, toPosition: Int) {
+ // TODO: Trigger this callback so we can assert state at this point as well
+ }
+
+ override fun onInserted(position: Int, count: Int) {
+ onInsertedSnapshot = differ.snapshot()
+ }
+
+ override fun onRemoved(position: Int, count: Int) {
+ onRemovedSnapshot = differ.snapshot()
+ }
+ }
+
+ val differ = AsyncPagingDataDiffer(
+ diffCallback = object : DiffUtil.ItemCallback<Int>() {
+ override fun areContentsTheSame(oldItem: Int, newItem: Int): Boolean {
+ return oldItem == newItem
+ }
+
+ override fun areItemsTheSame(oldItem: Int, newItem: Int): Boolean {
+ return oldItem == newItem
+ }
+ },
+ updateCallback = listUpdateCallback,
+ mainDispatcher = Dispatchers.Main,
+ workerDispatcher = Dispatchers.Main,
+ ).also {
+ listUpdateCallback.differ = it
+ }
+
+ // Initial insert; this only triggers onInserted
+ differ.submitData(PagingData.from(listOf(0)))
+ advanceUntilIdle()
+
+ val firstList = ItemSnapshotList(0, 0, listOf(0))
+ assertEquals(firstList, differ.snapshot())
+ assertEquals(firstList, onInsertedSnapshot)
+ assertEquals(initialSnapshot, onRemovedSnapshot)
+
+ // Switch item to 1; this triggers onInserted + onRemoved
+ differ.submitData(PagingData.from(listOf(1)))
+ advanceUntilIdle()
+
+ val secondList = ItemSnapshotList(0, 0, listOf(1))
+ assertEquals(secondList, differ.snapshot())
+ assertEquals(secondList, onInsertedSnapshot)
+ assertEquals(secondList, onRemovedSnapshot)
+ }
+ }
}
\ No newline at end of file
diff --git a/paging/runtime/src/androidTest/java/androidx/paging/StateRestorationTest.kt b/paging/runtime/src/androidTest/java/androidx/paging/StateRestorationTest.kt
new file mode 100644
index 0000000..f7df7ee
--- /dev/null
+++ b/paging/runtime/src/androidTest/java/androidx/paging/StateRestorationTest.kt
@@ -0,0 +1,530 @@
+/*
+ * 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.paging
+
+import android.app.Application
+import android.content.Context
+import android.os.Parcelable
+import android.view.View
+import android.view.View.MeasureSpec.EXACTLY
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.ALLOW
+import androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.PREVENT
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.internal.ThreadSafeHeap
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestCoroutineDispatcher
+import kotlinx.coroutines.test.TestCoroutineScope
+import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.withContext
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.coroutines.ContinuationInterceptor
+import kotlin.coroutines.CoroutineContext
+import kotlin.time.ExperimentalTime
+
+/**
+ * We are only capable of restoring state if one the two is valid:
+ * a) pager's flow is cached in the view model (only for config change)
+ * b) data source is counted and placeholders are enabled (both config change and app restart)
+ *
+ * Both of these cases actually work without using the initial key, except it is relatively
+ * slower in option B because we need to load all items from initial key to the required position.
+ *
+ * This test validates those two cases for now. For more complicated cases, we need some helper
+ * as developer needs to intervene to provide more information.
+ */
+@ExperimentalCoroutinesApi
+@ExperimentalTime
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class StateRestorationTest {
+ /**
+ * List of dispatchers we track in the test for idling + pushing execution.
+ * We have 3 dispatchers for more granular control:
+ * main, and background for pager.
+ * testScope for running tests.
+ */
+ private val trackedDispatchers = mutableListOf<TestCoroutineDispatcher>()
+
+ private val mainDispatcher = TestCoroutineDispatcher().track()
+ private val backgroundDispatcher = TestCoroutineDispatcher().track()
+ private val testScope = TestCoroutineScope().track()
+
+ /**
+ * A fake lifecycle scope for collections that get cancelled when we recreate the recyclerview.
+ */
+ private lateinit var lifecycleScope: TestCoroutineScope
+ private lateinit var recyclerView: TestRecyclerView
+ private lateinit var layoutManager: RestoreAwareLayoutManager
+ private lateinit var adapter: TestAdapter
+
+ /**
+ * tracks [this] dispatcher for idling control.
+ */
+ private fun TestCoroutineDispatcher.track() = apply {
+ trackedDispatchers.add(this)
+ }
+
+ /**
+ * tracks the dispatcher of this scope for idling control.
+ */
+ private fun TestCoroutineScope.track() = apply {
+ (this@track.coroutineContext[ContinuationInterceptor.Key] as TestCoroutineDispatcher)
+ .track()
+ }
+
+ @Before
+ fun init() {
+ createRecyclerView()
+ }
+
+ @Test
+ fun restoreState_withPlaceholders() {
+ runTest {
+ collectPagesAsync(
+ createPager(
+ pageSize = 100,
+ enablePlaceholders = true
+ ).flow
+ )
+ measureAndLayout()
+ val visible = recyclerView.captureSnapshot()
+ assertThat(visible).isNotEmpty()
+ scrollToPosition(50)
+ val expected = recyclerView.captureSnapshot()
+ saveAndRestore()
+ // make sure state is not restored before items are loaded
+ assertThat(
+ layoutManager.restoredState
+ ).isFalse()
+ backgroundDispatcher.pauseDispatcher()
+ collectPagesAsync(
+ createPager(
+ pageSize = 10,
+ enablePlaceholders = true
+ ).flow
+ )
+ measureAndLayout()
+ // background worker is blocked, still shouldn't restore state
+ assertThat(
+ layoutManager.restoredState
+ ).isFalse()
+ backgroundDispatcher.resumeDispatcher()
+ measureAndLayout()
+ assertThat(
+ layoutManager.restoredState
+ ).isTrue()
+ assertThat(
+ recyclerView.captureSnapshot()
+ ).containsExactlyElementsIn(
+ expected
+ )
+ }
+ }
+
+ @Test
+ fun restoreState_withoutPlaceholders_cachedIn() {
+ runTest {
+ val pager = createPager(
+ pageSize = 60,
+ enablePlaceholders = false
+ )
+ val cacheScope = TestCoroutineScope(Job()).track()
+ val cachedFlow = pager.flow.cachedIn(cacheScope)
+ collectPagesAsync(cachedFlow)
+ measureAndLayout()
+ // now scroll
+ scrollToPosition(50)
+ val snapshot = recyclerView.captureSnapshot()
+ saveAndRestore()
+ assertThat(
+ layoutManager.restoredState
+ ).isFalse()
+ collectPagesAsync(cachedFlow)
+ measureAndLayout()
+ assertThat(
+ layoutManager.restoredState
+ ).isTrue()
+ val restoredSnapshot = recyclerView.captureSnapshot()
+ assertThat(restoredSnapshot).containsExactlyElementsIn(snapshot)
+ cacheScope.cancel()
+ }
+ }
+
+ @Test
+ fun emptyNewPage_allowRestoration() {
+ // check that we don't block restoration indefinitely if new pager is empty.
+ runTest {
+ val pager = createPager(
+ pageSize = 60,
+ enablePlaceholders = true
+ )
+ collectPagesAsync(pager.flow)
+ measureAndLayout()
+ scrollToPosition(50)
+ saveAndRestore()
+ assertThat(
+ layoutManager.restoredState
+ ).isFalse()
+ val emptyPager = createPager(
+ pageSize = 10,
+ itemCount = 0,
+ enablePlaceholders = true
+ )
+ collectPagesAsync(emptyPager.flow)
+ measureAndLayout()
+ assertThat(
+ layoutManager.restoredState
+ ).isTrue()
+ }
+ }
+
+ @Test
+ fun userOverridesStateRestoration() {
+ runTest {
+ val pager = createPager(
+ pageSize = 40,
+ enablePlaceholders = true
+ )
+ collectPagesAsync(pager.flow)
+ measureAndLayout()
+ scrollToPosition(20)
+ val snapshot = recyclerView.captureSnapshot()
+ saveAndRestore()
+ val pager2 = createPager(
+ pageSize = 40,
+ enablePlaceholders = true
+ )
+ // when user calls prevent, we should not trigger state restoration even after we
+ // receive the first page
+ adapter.stateRestorationPolicy = PREVENT
+ collectPagesAsync(pager2.flow)
+ measureAndLayout()
+ assertThat(
+ layoutManager.restoredState
+ ).isFalse()
+ // make sure test did work as expected, that is, new items are loaded
+ assertThat(adapter.itemCount).isGreaterThan(0)
+ // now if user allows it, restoration should happen properly
+ adapter.stateRestorationPolicy = ALLOW
+ measureAndLayout()
+ assertThat(
+ layoutManager.restoredState
+ ).isTrue()
+ assertThat(recyclerView.captureSnapshot()).isEqualTo(snapshot)
+ }
+ }
+
+ private fun createRecyclerView() {
+ // cancel previous lifecycle if it exists
+ if (this::lifecycleScope.isInitialized) {
+ this.lifecycleScope.cancel()
+ }
+ lifecycleScope = TestCoroutineScope(
+ SupervisorJob() + mainDispatcher
+ ).track()
+ val context = ApplicationProvider.getApplicationContext<Application>()
+ recyclerView = TestRecyclerView(context)
+ recyclerView.itemAnimator = null
+ adapter = TestAdapter()
+ recyclerView.adapter = adapter
+ layoutManager = RestoreAwareLayoutManager(context)
+ recyclerView.layoutManager = layoutManager
+ }
+
+ private fun runPending() {
+ while (trackedDispatchers.any { it.isNotEmpty && it.isNotPaused }) {
+ trackedDispatchers.filter { it.isNotPaused }.forEach {
+ it.runCurrent()
+ }
+ }
+ }
+
+ private fun scrollToPosition(pos: Int) {
+ while (adapter.itemCount <= pos) {
+ val prevSize = adapter.itemCount
+ adapter.triggerItemLoad(prevSize - 1)
+ runPending()
+ // this might be an issue with dropping but it is not the case here
+ assertWithMessage("more items should be loaded")
+ .that(adapter.itemCount)
+ .isGreaterThan(prevSize)
+ }
+ runPending()
+ recyclerView.scrollToPosition(pos)
+ measureAndLayout()
+ val child = layoutManager.findViewByPosition(pos)
+ assertWithMessage("scrolled child $pos exists")
+ .that(child)
+ .isNotNull()
+
+ val vh = recyclerView.getChildViewHolder(child!!) as ItemViewHolder
+ assertWithMessage("scrolled child should be fully loaded")
+ .that(vh.item)
+ .isNotNull()
+ }
+
+ private fun measureAndLayout() {
+ runPending()
+ while (recyclerView.isLayoutRequested) {
+ measure()
+ layout()
+ runPending()
+ }
+ }
+
+ private fun measure() {
+ recyclerView.measure(EXACTLY or RV_WIDTH, EXACTLY or RV_HEIGHT)
+ }
+
+ private fun layout() {
+ recyclerView.layout(0, 0, 100, 200)
+ }
+
+ private fun saveAndRestore() {
+ val state = recyclerView.saveState()
+ createRecyclerView()
+ recyclerView.restoreState(state)
+ measureAndLayout()
+ }
+
+ private fun runTest(block: TestCoroutineScope.() -> Unit) {
+ testScope.runBlockingTest {
+ try {
+ this.block()
+ } finally {
+ runPending()
+ // always cancel the lifecycle scope to ensure any collection there ends
+ if (this@StateRestorationTest::lifecycleScope.isInitialized) {
+ lifecycleScope.cancel()
+ }
+ }
+ }
+ }
+
+ /**
+ * collects pages in the lifecycle scope and sends them to the adapter
+ */
+ private fun collectPagesAsync(
+ flow: Flow<PagingData<Item>>
+ ) {
+ val targetAdapter = adapter
+ lifecycleScope.launch {
+ flow.collectLatest {
+ targetAdapter.submitData(it)
+ }
+ }
+ }
+
+ private fun createPager(
+ pageSize: Int,
+ enablePlaceholders: Boolean,
+ itemCount: Int = 100,
+ initialKey: Int? = null
+ ): Pager<Int, Item> {
+ return Pager(
+ config = PagingConfig(
+ pageSize = pageSize,
+ enablePlaceholders = enablePlaceholders,
+ ),
+ initialKey = initialKey,
+ pagingSourceFactory = {
+ ItemPagingSource(
+ context = backgroundDispatcher,
+ items = (0 until itemCount).map { Item(it) }
+ )
+ }
+ )
+ }
+
+ /**
+ * Returns the list of all visible items in the recyclerview including their locations.
+ */
+ private fun RecyclerView.captureSnapshot(): List<PositionSnapshot> {
+ return (0 until childCount).mapNotNull {
+ val child = getChildAt(it)
+ // if child is not visible, ignore it as RV might have extra views around the visible
+ // area.
+ if (child.top >= height || child.bottom <= 0) {
+ // not visible, ignore
+ null
+ } else {
+ val vh = getChildViewHolder(child)
+ (vh as ItemViewHolder).captureSnapshot()
+ }
+ }
+ }
+
+ class ItemView(context: Context) : View(context)
+
+ class ItemViewHolder(context: Context) : RecyclerView.ViewHolder(ItemView(context)) {
+ var item: Item? = null
+ private set
+
+ init {
+ itemView.layoutParams = RecyclerView.LayoutParams(0, 0)
+ }
+
+ fun captureSnapshot(): PositionSnapshot {
+ val item = checkNotNull(item)
+ return PositionSnapshot(
+ item = item,
+ top = itemView.top,
+ bottom = itemView.bottom
+ )
+ }
+
+ fun bindTo(item: Item?) {
+ this.item = item
+ // setting placeholder height to 0 creates a weird jumping bug, investigate
+ itemView.layoutParams.height = item?.height ?: RV_HEIGHT / 10
+ }
+ }
+
+ /**
+ * Checks whether a [TestCoroutineDispatcher] has any pending actions using reflection :)
+ */
+ @OptIn(InternalCoroutinesApi::class)
+ private val TestCoroutineDispatcher.isNotEmpty: Boolean
+ get() {
+ this@isNotEmpty::class.java.getDeclaredField("queue").let {
+ it.isAccessible = true
+ val heap = it.get(this) as ThreadSafeHeap<*>
+ return !heap.isEmpty
+ }
+ }
+
+ /**
+ * Checks whether a [TestCoroutineDispatcher] is paused or not using reflection.
+ */
+ private val TestCoroutineDispatcher.isNotPaused: Boolean
+ get() {
+ this@isNotPaused::class.java.getDeclaredField("dispatchImmediately").let {
+ it.isAccessible = true
+ return it.get(this) as Boolean
+ }
+ }
+
+ data class Item(
+ val id: Int,
+ val height: Int = (RV_HEIGHT / 10) + (1 + (id % 10))
+ ) {
+ companion object {
+ val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Item>() {
+ override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
+ return oldItem.id == newItem.id
+ }
+
+ override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
+ return oldItem == newItem
+ }
+ }
+ }
+ }
+
+ inner class TestAdapter : PagingDataAdapter<Item, ItemViewHolder>(
+ diffCallback = Item.DIFF_CALLBACK,
+ mainDispatcher = mainDispatcher,
+ workerDispatcher = backgroundDispatcher
+ ) {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
+ return ItemViewHolder(parent.context)
+ }
+
+ override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
+ holder.bindTo(getItem(position))
+ }
+
+ fun triggerItemLoad(pos: Int) = super.getItem(pos)
+ }
+
+ class ItemPagingSource(
+ private val context: CoroutineContext,
+ private val items: List<Item>
+ ) : PagingSource<Int, Item>() {
+ override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Item> {
+ return withContext(context) {
+ val key = params.key ?: 0
+ val isPrepend = params is LoadParams.Prepend
+ val start = if (isPrepend) key - params.loadSize + 1 else key
+ val end = if (isPrepend) key + 1 else key + params.loadSize
+
+ LoadResult.Page(
+ data = items.subList(maxOf(0, start), minOf(end, items.size)),
+ prevKey = if (start > 0) start - 1 else null,
+ nextKey = if (end < items.size) end else null,
+ itemsBefore = maxOf(0, start),
+ itemsAfter = maxOf(0, items.size - end)
+ )
+ }
+ }
+ }
+
+ /**
+ * Snapshot of an item in RecyclerView.
+ */
+ data class PositionSnapshot(
+ val item: Item,
+ val top: Int,
+ val bottom: Int
+ )
+
+ /**
+ * RecyclerView class that allows saving and restoring state.
+ */
+ class TestRecyclerView(context: Context) : RecyclerView(context) {
+ fun restoreState(state: Parcelable?) {
+ super.onRestoreInstanceState(state)
+ }
+
+ fun saveState(): Parcelable? {
+ return super.onSaveInstanceState()
+ }
+ }
+
+ /**
+ * A layout manager that tracks whether state is restored or not so that we can assert on it.
+ */
+ class RestoreAwareLayoutManager(context: Context) : LinearLayoutManager(context) {
+ var restoredState = false
+ override fun onRestoreInstanceState(state: Parcelable?) {
+ super.onRestoreInstanceState(state)
+ restoredState = true
+ }
+ }
+
+ companion object {
+ private const val RV_HEIGHT = 200
+ private const val RV_WIDTH = 100
+ }
+}
\ No newline at end of file
diff --git a/paging/runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt b/paging/runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt
index 2a5de36..8cb757b 100644
--- a/paging/runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt
+++ b/paging/runtime/src/main/java/androidx/paging/AsyncPagingDataDiffer.kt
@@ -71,15 +71,18 @@
previousList: NullPaddedList<T>,
newList: NullPaddedList<T>,
newCombinedLoadStates: CombinedLoadStates,
- lastAccessedIndex: Int
+ lastAccessedIndex: Int,
+ onListPresentable: () -> Unit,
) = when {
// fast path for no items -> some items
previousList.size == 0 -> {
+ onListPresentable()
differCallback.onInserted(0, newList.size)
null
}
// fast path for some items -> no items
newList.size == 0 -> {
+ onListPresentable()
differCallback.onRemoved(0, previousList.size)
null
}
@@ -87,6 +90,7 @@
val diffResult = withContext(workerDispatcher) {
previousList.computeDiff(newList, diffCallback)
}
+ onListPresentable()
previousList.dispatchDiff(updateCallback, newList, diffResult)
previousList.transformAnchorIndex(
diffResult = diffResult,
diff --git a/paging/runtime/src/main/java/androidx/paging/LivePagedList.kt b/paging/runtime/src/main/java/androidx/paging/LivePagedList.kt
index 32bfa57..ab56fae 100644
--- a/paging/runtime/src/main/java/androidx/paging/LivePagedList.kt
+++ b/paging/runtime/src/main/java/androidx/paging/LivePagedList.kt
@@ -41,10 +41,12 @@
private val fetchDispatcher: CoroutineDispatcher
) : LiveData<PagedList<Value>>(
InitialPagedList(
- pagingSourceFactory(),
- coroutineScope,
- config,
- initialKey
+ pagingSource = pagingSourceFactory(),
+ coroutineScope = coroutineScope,
+ notifyDispatcher = notifyDispatcher,
+ backgroundDispatcher = fetchDispatcher,
+ config = config,
+ initialLastKey = initialKey
)
) {
private var currentData: PagedList<Value>
diff --git a/paging/runtime/src/main/java/androidx/paging/PagingDataAdapter.kt b/paging/runtime/src/main/java/androidx/paging/PagingDataAdapter.kt
index 3f19e4b..82892f3 100644
--- a/paging/runtime/src/main/java/androidx/paging/PagingDataAdapter.kt
+++ b/paging/runtime/src/main/java/androidx/paging/PagingDataAdapter.kt
@@ -46,6 +46,14 @@
* compute fine grained updates as updated content in the form of new PagingData objects are
* received.
*
+ * *State Restoration*: To be able to restore [RecyclerView] state (e.g. scroll position) after a
+ * configuration change / application recreate, [PagingDataAdapter] calls
+ * [RecyclerView.Adapter.setStateRestorationPolicy] with
+ * [RecyclerView.Adapter.StateRestorationPolicy.PREVENT] upon initialization and waits for the
+ * first page to load before allowing state restoration.
+ * Any other call to [RecyclerView.Adapter.setStateRestorationPolicy] by the application will
+ * disable this logic and will rely on the user set value.
+ *
* @sample androidx.paging.samples.pagingDataAdapterSample
*/
abstract class PagingDataAdapter<T : Any, VH : RecyclerView.ViewHolder> @JvmOverloads constructor(
@@ -53,6 +61,41 @@
mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
workerDispatcher: CoroutineDispatcher = Dispatchers.Default
) : RecyclerView.Adapter<VH>() {
+
+ init {
+ super.setStateRestorationPolicy(StateRestorationPolicy.PREVENT)
+ // prevent state restoration and then watch for the first insert event.
+ // differ calls this with the inserted page even when it is empty, which is what we want
+ // here
+ @Suppress("LeakingThis")
+ registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
+ override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
+ considerAllowingStateRestoration()
+ unregisterAdapterDataObserver(this)
+ super.onItemRangeInserted(positionStart, itemCount)
+ }
+
+ private fun considerAllowingStateRestoration() {
+ if (stateRestorationPolicy == StateRestorationPolicy.PREVENT &&
+ !userSetRestorationPolicy
+ ) {
+ this@PagingDataAdapter.setStateRestorationPolicy(StateRestorationPolicy.ALLOW)
+ }
+ }
+ })
+ }
+
+ /**
+ * Track whether developer called [setStateRestorationPolicy] or not to decide whether the
+ * automated state restoration should apply or not.
+ */
+ private var userSetRestorationPolicy = false
+
+ override fun setStateRestorationPolicy(strategy: StateRestorationPolicy) {
+ userSetRestorationPolicy = true
+ super.setStateRestorationPolicy(strategy)
+ }
+
private val differ = AsyncPagingDataDiffer(
diffCallback = diffCallback,
updateCallback = AdapterListUpdateCallback(this),
diff --git a/paging/rxjava2/src/main/java/androidx/paging/RxPagedListBuilder.kt b/paging/rxjava2/src/main/java/androidx/paging/RxPagedListBuilder.kt
index 1a8211d..2959747 100644
--- a/paging/rxjava2/src/main/java/androidx/paging/RxPagedListBuilder.kt
+++ b/paging/rxjava2/src/main/java/androidx/paging/RxPagedListBuilder.kt
@@ -356,10 +356,12 @@
init {
currentData = InitialPagedList(
- pagingSourceFactory(),
- GlobalScope,
- config,
- initialLoadKey
+ pagingSource = pagingSourceFactory(),
+ coroutineScope = GlobalScope,
+ notifyDispatcher = notifyDispatcher,
+ backgroundDispatcher = fetchDispatcher,
+ config = config,
+ initialLastKey = initialLoadKey
)
currentData.setRetryCallback(refreshRetryCallback)
}
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index 03609a8..0c651335 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -28,7 +28,7 @@
androidx.enableDocumentation=false
# Disable coverage
androidx.coverageEnabled=false
-androidx.playground.snapshotBuildId=6990786
+androidx.playground.snapshotBuildId=7012196
androidx.playground.metalavaBuildId=6990868
androidx.playground.dokkaBuildId=6915080
androidx.studio.type=playground
diff --git a/room/compiler-processing-testing/build.gradle b/room/compiler-processing-testing/build.gradle
index 0edce59..44e7780 100644
--- a/room/compiler-processing-testing/build.gradle
+++ b/room/compiler-processing-testing/build.gradle
@@ -26,12 +26,18 @@
dependencies {
implementation("androidx.annotation:annotation:1.1.0")
- implementation(project(":room:room-compiler-processing"))
+ api(project(":room:room-compiler-processing"))
implementation(KOTLIN_STDLIB)
implementation(KOTLIN_KSP_API)
- testImplementation(KOTLIN_KSP)
+ implementation(KOTLIN_KSP)
implementation(GOOGLE_COMPILE_TESTING)
implementation(KOTLIN_COMPILE_TESTING_KSP)
+ // specify these because KSP do not specify them and we might get an older version from kotlin
+ // compile testing
+ // https://github.com/google/ksp/issues/187
+ implementation(KOTLIN_COMPILER_EMBEDDABLE)
+ implementation(KOTLIN_COMPILER_DAEMON_EMBEDDABLE)
+ implementation(KOTLIN_ANNOTATION_PROCESSING_EMBEDDABLE)
}
androidx {
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticJavacProcessor.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticJavacProcessor.kt
index 0ac5928..54fc124 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticJavacProcessor.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticJavacProcessor.kt
@@ -16,21 +16,27 @@
package androidx.room.compiler.processing
+import androidx.room.compiler.processing.util.RecordingXMessager
import androidx.room.compiler.processing.util.XTestInvocation
-import java.lang.AssertionError
import javax.lang.model.SourceVersion
class SyntheticJavacProcessor(
- val handler: (XTestInvocation) -> Unit
-) : JavacTestProcessor() {
+ val handler: (XTestInvocation) -> Unit,
+) : JavacTestProcessor(), SyntheticProcessor {
+ override val invocationInstances = mutableListOf<XTestInvocation>()
private var result: Result<Unit>? = null
+ override val messageWatcher = RecordingXMessager()
override fun doProcess(annotations: Set<XTypeElement>, roundEnv: XRoundEnv): Boolean {
+ val xEnv = XProcessingEnv.create(processingEnv)
+ xEnv.messager.addMessageWatcher(messageWatcher)
result = kotlin.runCatching {
handler(
XTestInvocation(
- processingEnv = XProcessingEnv.create(processingEnv)
- )
+ processingEnv = xEnv
+ ).also {
+ invocationInstances.add(it)
+ }
)
}
return true
@@ -42,7 +48,7 @@
override fun getSupportedAnnotationTypes() = setOf("*")
- fun throwIfFailed() {
+ override fun throwIfFailed() {
val result = checkNotNull(result) {
"did not compile"
}
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticKspProcessor.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticKspProcessor.kt
index 106d6b1..68ec813 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticKspProcessor.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticKspProcessor.kt
@@ -16,6 +16,7 @@
package androidx.room.compiler.processing
+import androidx.room.compiler.processing.util.RecordingXMessager
import androidx.room.compiler.processing.util.XTestInvocation
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.KSPLogger
@@ -24,11 +25,14 @@
class SyntheticKspProcessor(
private val handler: (XTestInvocation) -> Unit
-) : SymbolProcessor {
+) : SymbolProcessor, SyntheticProcessor {
+ override val invocationInstances = mutableListOf<XTestInvocation>()
private var result: Result<Unit>? = null
private lateinit var options: Map<String, String>
private lateinit var codeGenerator: CodeGenerator
private lateinit var logger: KSPLogger
+ override val messageWatcher = RecordingXMessager()
+
override fun finish() {
}
@@ -44,21 +48,25 @@
}
override fun process(resolver: Resolver) {
+ val xEnv = XProcessingEnv.create(
+ options,
+ resolver,
+ codeGenerator,
+ logger
+ )
+ xEnv.messager.addMessageWatcher(messageWatcher)
result = kotlin.runCatching {
handler(
XTestInvocation(
- processingEnv = XProcessingEnv.create(
- options,
- resolver,
- codeGenerator,
- logger
- )
- )
+ processingEnv = xEnv
+ ).also {
+ invocationInstances.add(it)
+ }
)
}
}
- fun throwIfFailed() {
+ override fun throwIfFailed() {
val result = checkNotNull(result) {
"did not compile"
}
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticProcessor.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticProcessor.kt
new file mode 100644
index 0000000..05cc826
--- /dev/null
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/SyntheticProcessor.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.util.RecordingXMessager
+import androidx.room.compiler.processing.util.XTestInvocation
+
+/**
+ * Common interface for SyntheticProcessors that we create for testing.
+ */
+internal interface SyntheticProcessor {
+ /**
+ * List of invocations that was sent to the test code.
+ *
+ * The test code can register assertions on the compilation result, which is why we need this
+ * list (to run assertions after compilation).
+ */
+ val invocationInstances: List<XTestInvocation>
+
+ /**
+ * The recorder for messages where we'll grab the diagnostics.
+ */
+ val messageWatcher: RecordingXMessager
+
+ /**
+ * Should throw if processor did throw an exception.
+ * When assertions fail, we don't fail the compilation to keep the stack trace, instead,
+ * dispatch them afterwards.
+ */
+ fun throwIfFailed()
+}
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/CompilationResultSubject.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/CompilationResultSubject.kt
new file mode 100644
index 0000000..3dbbff7
--- /dev/null
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/CompilationResultSubject.kt
@@ -0,0 +1,210 @@
+/*
+ * 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.util
+
+import androidx.room.compiler.processing.SyntheticJavacProcessor
+import androidx.room.compiler.processing.SyntheticProcessor
+import androidx.room.compiler.processing.util.runner.CompilationTestRunner
+import com.google.common.truth.Fact.simpleFact
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.Subject
+import com.google.common.truth.Subject.Factory
+import com.google.common.truth.Truth
+import com.google.testing.compile.Compilation
+import com.google.testing.compile.CompileTester
+import com.tschuchort.compiletesting.KotlinCompilation
+import javax.tools.Diagnostic
+
+/**
+ * Holds the information about a test compilation result.
+ */
+abstract class CompilationResult internal constructor(
+ /**
+ * The test infra which run this test
+ */
+ internal val testRunnerName: String,
+ /**
+ * The [SyntheticProcessor] used in this compilation.
+ */
+ internal val processor: SyntheticProcessor,
+ /**
+ * True if compilation result was success.
+ */
+ internal val successfulCompilation: Boolean,
+) {
+ private val diagnostics = processor.messageWatcher.diagnostics()
+
+ fun diagnosticsOfKind(kind: Diagnostic.Kind) = diagnostics[kind].orEmpty()
+
+ override fun toString(): String {
+ return buildString {
+ appendLine("CompilationResult (with $testRunnerName)")
+ Diagnostic.Kind.values().forEach { kind ->
+ val messages = diagnosticsOfKind(kind)
+ appendLine("${kind.name}: ${messages.size}")
+ messages.forEach {
+ appendLine(it)
+ }
+ appendLine()
+ }
+ }
+ }
+}
+
+/**
+ * Truth subject that can run assertions on the [CompilationResult].
+ * see: [XTestInvocation.assertCompilationResult]
+ */
+class CompilationResultSubject(
+ failureMetadata: FailureMetadata,
+ val compilationResult: CompilationResult,
+) : Subject<CompilationResultSubject, CompilationResult>(
+ failureMetadata, compilationResult
+) {
+ /**
+ * set to true if any assertion on the subject requires it to fail (e.g. looking for errors)
+ */
+ internal var shouldSucceed: Boolean = true
+
+ /**
+ * Asserts that compilation did fail. This covers the cases where the processor won't print
+ * any diagnostics but compilation will still fail (e.g. bad generated code).
+ *
+ * @see hasError
+ */
+ fun compilationDidFail() = chain {
+ shouldSucceed = false
+ }
+
+ /**
+ * Asserts that compilation has a warning with the given text.
+ *
+ * @see hasError
+ */
+ fun hasWarning(expected: String) = chain {
+ hasDiagnosticWithMessage(
+ kind = Diagnostic.Kind.WARNING,
+ expected = expected
+ ) {
+ "expected warning: $expected"
+ }
+ }
+
+ /**
+ * Asserts that compilation has an error with the given text.
+ *
+ * @see hasWarning
+ */
+ fun hasError(expected: String) = chain {
+ shouldSucceed = false
+ hasDiagnosticWithMessage(
+ kind = Diagnostic.Kind.ERROR,
+ expected = expected
+ ) {
+ "expected error: $expected"
+ }
+ }
+
+ /**
+ * Asserts that compilation has at least one diagnostics message with kind error.
+ *
+ * @see compilationDidFail
+ * @see hasWarning
+ */
+ fun hasError() = chain {
+ shouldSucceed = false
+ if (actual().diagnosticsOfKind(Diagnostic.Kind.ERROR).isEmpty()) {
+ failWithActual(
+ simpleFact("expected at least one failure message")
+ )
+ }
+ }
+
+ /**
+ * Called after handler is invoked to check its compilation failure assertion against the
+ * compilation result.
+ */
+ internal fun assertCompilationResult() {
+ if (compilationResult.successfulCompilation != shouldSucceed) {
+ failWithActual(
+ simpleFact(
+ "expected compilation result to be: $shouldSucceed but was " +
+ "${compilationResult.successfulCompilation}"
+ )
+ )
+ }
+ }
+
+ private fun hasDiagnosticWithMessage(
+ kind: Diagnostic.Kind,
+ expected: String,
+ buildErrorMessage: () -> String
+ ) {
+ val diagnostics = compilationResult.diagnosticsOfKind(kind)
+ if (diagnostics.any { it.msg == expected }) {
+ return
+ }
+ failWithActual(simpleFact(buildErrorMessage()))
+ }
+
+ private fun chain(
+ block: () -> Unit
+ ): CompileTester.ChainingClause<CompilationResultSubject> {
+ block()
+ return CompileTester.ChainingClause<CompilationResultSubject> {
+ this
+ }
+ }
+
+ companion object {
+ private val FACTORY =
+ Factory<CompilationResultSubject, CompilationResult> { metadata, actual ->
+ CompilationResultSubject(metadata, actual)
+ }
+
+ fun assertThat(
+ compilationResult: CompilationResult
+ ): CompilationResultSubject {
+ return Truth.assertAbout(FACTORY).that(
+ compilationResult
+ )
+ }
+ }
+}
+
+internal class JavaCompileTestingCompilationResult(
+ testRunner: CompilationTestRunner,
+ @Suppress("unused")
+ private val delegate: Compilation,
+ processor: SyntheticJavacProcessor
+) : CompilationResult(
+ testRunnerName = testRunner.name,
+ processor = processor,
+ successfulCompilation = delegate.status() == Compilation.Status.SUCCESS
+)
+
+internal class KotlinCompileTestingCompilationResult(
+ testRunner: CompilationTestRunner,
+ @Suppress("unused")
+ private val delegate: KotlinCompilation.Result,
+ processor: SyntheticProcessor,
+ successfulCompilation: Boolean
+) : CompilationResult(
+ testRunnerName = testRunner.name,
+ processor = processor,
+ successfulCompilation = successfulCompilation
+)
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/DiagnosticMessage.kt
similarity index 73%
copy from compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
copy to room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/DiagnosticMessage.kt
index f9cb2fe..34ef8e3 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/DiagnosticMessage.kt
@@ -14,7 +14,14 @@
* limitations under the License.
*/
-package androidx.compose.ui.focus
+package androidx.room.compiler.processing.util
-@RequiresOptIn("The Focus API is experimental and is likely to change in the future.")
-annotation class ExperimentalFocus
\ No newline at end of file
+import androidx.room.compiler.processing.XElement
+
+/**
+ * Holder for diagnostics messages
+ */
+data class DiagnosticMessage(
+ val msg: String,
+ val element: XElement?
+)
\ No newline at end of file
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt
index 076b531..bbf2888 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/ProcessorTestExt.kt
@@ -16,275 +16,147 @@
package androidx.room.compiler.processing.util
-import androidx.room.compiler.processing.SyntheticJavacProcessor
-import androidx.room.compiler.processing.SyntheticKspProcessor
-import com.google.common.truth.Truth
+import androidx.room.compiler.processing.util.runner.CompilationTestRunner
+import androidx.room.compiler.processing.util.runner.JavacCompilationTestRunner
+import androidx.room.compiler.processing.util.runner.KaptCompilationTestRunner
+import androidx.room.compiler.processing.util.runner.KspCompilationTestRunner
+import androidx.room.compiler.processing.util.runner.TestCompilationParameters
import com.google.common.truth.Truth.assertThat
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaSourcesSubjectFactory
+import com.google.common.truth.Truth.assertWithMessage
import com.tschuchort.compiletesting.KotlinCompilation
-import com.tschuchort.compiletesting.SourceFile
-import com.tschuchort.compiletesting.kspSourcesDir
-import com.tschuchort.compiletesting.symbolProcessors
import java.io.File
-// TODO get rid of these once kotlin compile testing supports two step compilation for KSP.
-// https://github.com/tschuchortdev/kotlin-compile-testing/issues/72
-private val KotlinCompilation.kspJavaSourceDir: File
- get() = kspSourcesDir.resolve("java")
+private fun runTests(
+ params: TestCompilationParameters,
+ vararg runners: CompilationTestRunner
+) {
+ val runCount = runners.count { runner ->
+ if (runner.canRun(params)) {
+ val compilationResult = runner.compile(params)
+ val subject = CompilationResultSubject.assertThat(compilationResult)
+ // if any assertion failed, throw first those.
+ compilationResult.processor.throwIfFailed()
-private val KotlinCompilation.kspKotlinSourceDir: File
- get() = kspSourcesDir.resolve("kotlin")
+ compilationResult.processor.invocationInstances.forEach {
+ it.runPostCompilationChecks(subject)
+ }
+ assertWithMessage(
+ "compilation should've run the processor callback at least once"
+ ).that(
+ compilationResult.processor.invocationInstances
+ ).isNotEmpty()
-private fun compileSources(
- sources: List<Source>,
- classpath: List<File>,
+ subject.assertCompilationResult()
+ true
+ } else {
+ false
+ }
+ }
+ // make sure some tests did run
+ assertThat(runCount).isGreaterThan(0)
+}
+
+fun runProcessorTestWithoutKsp(
+ sources: List<Source> = emptyList(),
+ classpath: List<File> = emptyList(),
handler: (XTestInvocation) -> Unit
-): Pair<SyntheticJavacProcessor, CompileTester> {
- val syntheticJavacProcessor = SyntheticJavacProcessor(handler)
- return syntheticJavacProcessor to Truth.assertAbout(
- JavaSourcesSubjectFactory.javaSources()
- ).that(
- sources.map {
- it.toJFO()
- }
- ).apply {
- if (classpath.isNotEmpty()) {
- withClasspath(classpath)
- }
- }.processedWith(
- syntheticJavacProcessor
+) {
+ runTests(
+ params = TestCompilationParameters(
+ sources = sources,
+ classpath = classpath,
+ handler = handler
+ ),
+ JavacCompilationTestRunner,
+ KaptCompilationTestRunner
)
}
-private fun compileWithKapt(
- sources: List<Source>,
- classpath: List<File>,
- handler: (XTestInvocation) -> Unit
-): Pair<SyntheticJavacProcessor, KotlinCompilation> {
- val syntheticJavacProcessor = SyntheticJavacProcessor(handler)
- val compilation = KotlinCompilation()
- sources.forEach {
- compilation.workingDir.resolve("sources")
- .resolve(it.relativePath())
- .parentFile
- .mkdirs()
- }
- compilation.sources = sources.map {
- it.toKotlinSourceFile()
- }
- compilation.annotationProcessors = listOf(syntheticJavacProcessor)
- compilation.inheritClassPath = true
- compilation.verbose = false
- compilation.classpaths += classpath
-
- return syntheticJavacProcessor to compilation
-}
-
-private fun compileWithKsp(
- sources: List<Source>,
- classpath: List<File>,
- handler: (XTestInvocation) -> Unit
-): Pair<SyntheticKspProcessor, KotlinCompilation.Result> {
- @Suppress("NAME_SHADOWING")
- val sources = if (sources.none { it is Source.KotlinSource }) {
- // looks like this requires a kotlin source file
- // see: https://github.com/tschuchortdev/kotlin-compile-testing/issues/57
- sources + Source.kotlin("placeholder.kt", "")
- } else {
- sources
- }
- val syntheticKspProcessor = SyntheticKspProcessor(handler)
- fun prepareCompilation(): KotlinCompilation {
- val compilation = KotlinCompilation()
- sources.forEach {
- compilation.workingDir.resolve("sources")
- .resolve(it.relativePath())
- .parentFile
- .mkdirs()
- }
- compilation.sources = sources.map {
- it.toKotlinSourceFile()
- }
- compilation.jvmDefault = "enable"
- compilation.jvmTarget = "1.8"
- compilation.inheritClassPath = true
- compilation.verbose = false
- compilation.classpaths += classpath
- return compilation
- }
-
- val kspCompilation = prepareCompilation()
- kspCompilation.symbolProcessors = listOf(syntheticKspProcessor)
- kspCompilation.compile()
- // ignore KSP result for now because KSP stops compilation, which might create false negatives
- // when java code accesses kotlin code.
- // TODO: fix once https://github.com/tschuchortdev/kotlin-compile-testing/issues/72 is fixed
-
- // after ksp, compile without ksp with KSP's output as input
- val finalCompilation = prepareCompilation()
- // build source files from generated code
- finalCompilation.sources += kspCompilation.kspJavaSourceDir.collectSourceFiles() +
- kspCompilation.kspKotlinSourceDir.collectSourceFiles()
- return syntheticKspProcessor to finalCompilation.compile()
-}
-
-private fun File.collectSourceFiles(): List<SourceFile> {
- return walkTopDown().filter {
- it.isFile
- }.map { file ->
- SourceFile.fromPath(file)
- }.toList()
-}
-
+/**
+ * Runs the compilation test with all 3 backends (javac, kapt, ksp) if possible (e.g. javac
+ * cannot test kotlin sources).
+ *
+ * The [handler] will be invoked for each compilation hence it should be repeatable.
+ *
+ * To assert on the compilation results, [handler] can call
+ * [XTestInvocation.assertCompilationResult] where it will receive a subject for post compilation
+ * assertions.
+ *
+ * By default, the compilation is expected to succeed. If it should fail, there must be an
+ * assertion on [XTestInvocation.assertCompilationResult] which expects a failure (e.g. checking
+ * errors).
+ */
fun runProcessorTest(
sources: List<Source> = emptyList(),
classpath: List<File> = emptyList(),
handler: (XTestInvocation) -> Unit
) {
- @Suppress("NAME_SHADOWING")
- val sources = if (sources.isEmpty()) {
- // synthesize a source to trigger compilation
- listOf(
- Source.java(
- "foo.bar.SyntheticSource",
- """
- package foo.bar;
- public class SyntheticSource {}
- """.trimIndent()
- )
- )
- } else {
- sources
- }
- // we can compile w/ javac only if all code is in java
- if (sources.canCompileWithJava()) {
- runJavaProcessorTest(
+ runTests(
+ params = TestCompilationParameters(
sources = sources,
classpath = classpath,
- handler = handler,
- succeed = true
- )
- }
- runKaptTest(
- sources = sources,
- classpath = classpath,
- handler = handler,
- succeed = true
+ handler = handler
+ ),
+ JavacCompilationTestRunner,
+ KaptCompilationTestRunner,
+ KspCompilationTestRunner
)
}
/**
- * This method is oddly named instead of being an overload on runProcessorTest to easily track
- * which tests started to support KSP.
+ * Runs the test only with javac compilation backend.
*
- * Eventually, it will be merged with runProcessorTest when all tests pass with KSP.
+ * @see runProcessorTest
*/
-fun runProcessorTestIncludingKsp(
- sources: List<Source> = emptyList(),
- classpath: List<File> = emptyList(),
- handler: (XTestInvocation) -> Unit
-) {
- runProcessorTest(
- sources = sources,
- classpath = classpath,
- handler = handler
- )
- runKspTest(
- sources = sources,
- classpath = classpath,
- succeed = true,
- handler = handler
- )
-}
-
-fun runProcessorTestForFailedCompilation(
- sources: List<Source>,
- classpath: List<File> = emptyList(),
- handler: (XTestInvocation) -> Unit
-) {
- if (sources.canCompileWithJava()) {
- // run with java processor
- runJavaProcessorTest(
- sources = sources,
- classpath = classpath,
- handler = handler,
- succeed = false
- )
- }
- // now run with kapt
- runKaptTest(
- sources = sources,
- classpath = classpath,
- handler = handler,
- succeed = false
- )
-}
-
-fun runProcessorTestForFailedCompilationIncludingKsp(
- sources: List<Source>,
- classpath: List<File>,
- handler: (XTestInvocation) -> Unit
-) {
- runProcessorTestForFailedCompilation(
- sources = sources,
- classpath = classpath,
- handler = handler
- )
- // now run with ksp
- runKspTest(
- sources = sources,
- classpath = classpath,
- handler = handler,
- succeed = false
- )
-}
-
fun runJavaProcessorTest(
sources: List<Source>,
classpath: List<File>,
- succeed: Boolean,
handler: (XTestInvocation) -> Unit
) {
- val (syntheticJavacProcessor, compileTester) = compileSources(sources, classpath, handler)
- if (succeed) {
- compileTester.compilesWithoutError()
- } else {
- compileTester.failsToCompile()
- }
- syntheticJavacProcessor.throwIfFailed()
+ runTests(
+ params = TestCompilationParameters(
+ sources = sources,
+ classpath = classpath,
+ handler = handler
+ ),
+ JavacCompilationTestRunner
+ )
}
+/**
+ * Runs the test only with kapt compilation backend
+ */
fun runKaptTest(
sources: List<Source>,
classpath: List<File> = emptyList(),
- succeed: Boolean = true,
handler: (XTestInvocation) -> Unit
) {
- // now run with kapt
- val (kaptProcessor, kotlinCompilation) = compileWithKapt(sources, classpath, handler)
- val compilationResult = kotlinCompilation.compile()
- if (succeed) {
- assertThat(compilationResult.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
- } else {
- assertThat(compilationResult.exitCode).isNotEqualTo(KotlinCompilation.ExitCode.OK)
- }
- kaptProcessor.throwIfFailed()
+ runTests(
+ params = TestCompilationParameters(
+ sources = sources,
+ classpath = classpath,
+ handler = handler
+ ),
+ KaptCompilationTestRunner
+ )
}
+/**
+ * Runs the test only with ksp compilation backend
+ */
fun runKspTest(
sources: List<Source>,
classpath: List<File> = emptyList(),
- succeed: Boolean = true,
handler: (XTestInvocation) -> Unit
) {
- val (kspProcessor, compilationResult) = compileWithKsp(sources, classpath, handler)
- if (succeed) {
- assertThat(compilationResult.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
- } else {
- assertThat(compilationResult.exitCode).isNotEqualTo(KotlinCompilation.ExitCode.OK)
- }
- kspProcessor.throwIfFailed()
+ runTests(
+ params = TestCompilationParameters(
+ sources = sources,
+ classpath = classpath,
+ handler = handler
+ ),
+ KspCompilationTestRunner
+ )
}
/**
@@ -314,5 +186,3 @@
}
return compilation.classesDir
}
-
-private fun List<Source>.canCompileWithJava() = all { it is Source.JavaSource }
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/RecordingXMessager.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/RecordingXMessager.kt
new file mode 100644
index 0000000..c2aa5dc
--- /dev/null
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/RecordingXMessager.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.util
+
+import androidx.room.compiler.processing.XElement
+import androidx.room.compiler.processing.XMessager
+import javax.tools.Diagnostic
+
+/**
+ * An XMessager implementation that holds onto dispatched diagnostics.
+ */
+class RecordingXMessager : XMessager() {
+ private val diagnostics = mutableMapOf<Diagnostic.Kind, MutableList<DiagnosticMessage>>()
+
+ fun diagnostics(): Map<Diagnostic.Kind, List<DiagnosticMessage>> = diagnostics
+
+ override fun onPrintMessage(kind: Diagnostic.Kind, msg: String, element: XElement?) {
+ diagnostics.getOrPut(
+ kind
+ ) {
+ mutableListOf()
+ }.add(
+ DiagnosticMessage(
+ msg = msg,
+ element = element
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/XTestInvocation.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/XTestInvocation.kt
index 267c140..d7d0e75 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/XTestInvocation.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/XTestInvocation.kt
@@ -17,6 +17,7 @@
package androidx.room.compiler.processing.util
import androidx.room.compiler.processing.XProcessingEnv
+import kotlin.reflect.KClass
/**
* Data holder for XProcessing tests to access the processing environment.
@@ -24,6 +25,48 @@
class XTestInvocation(
val processingEnv: XProcessingEnv,
) {
+ /**
+ * Extension mechanism to allow putting objects into invocation that can be retrieved later.
+ */
+ private val userData = mutableMapOf<KClass<*>, Any>()
+
+ private val postCompilationAssertions = mutableListOf<CompilationResultSubject.() -> Unit>()
val isKsp: Boolean
get() = processingEnv.backend == XProcessingEnv.Backend.KSP
+
+ /**
+ * Registers a block that will be called with a [CompilationResultSubject] when compilation
+ * finishes.
+ *
+ * Note that it is not safe to access the environment in this block.
+ */
+ fun assertCompilationResult(block: CompilationResultSubject.() -> Unit) {
+ postCompilationAssertions.add(block)
+ }
+
+ internal fun runPostCompilationChecks(
+ compilationResultSubject: CompilationResultSubject
+ ) {
+ postCompilationAssertions.forEach {
+ it(compilationResultSubject)
+ }
+ }
+
+ fun <T : Any> getUserData(key: KClass<T>): T? {
+ @Suppress("UNCHECKED_CAST")
+ return userData[key] as T?
+ }
+
+ fun <T : Any> putUserData(key: KClass<T>, value: T) {
+ userData[key] = value
+ }
+
+ fun <T : Any> getOrPutUserData(key: KClass<T>, create: () -> T): T {
+ getUserData(key)?.let {
+ return it
+ }
+ return create().also {
+ putUserData(key, it)
+ }
+ }
}
\ No newline at end of file
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/CompilationTestRunner.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/CompilationTestRunner.kt
new file mode 100644
index 0000000..257e58a
--- /dev/null
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/CompilationTestRunner.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.util.runner
+
+import androidx.room.compiler.processing.util.CompilationResult
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import java.io.File
+
+/**
+ * Common interface for compilation tests
+ */
+internal interface CompilationTestRunner {
+ // user visible name that we can print in assertions
+ val name: String
+
+ fun canRun(params: TestCompilationParameters): Boolean
+
+ fun compile(params: TestCompilationParameters): CompilationResult
+}
+
+internal data class TestCompilationParameters(
+ val sources: List<Source> = emptyList(),
+ val classpath: List<File> = emptyList(),
+ val handler: (XTestInvocation) -> Unit
+)
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/JavacCompilationTestRunner.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/JavacCompilationTestRunner.kt
new file mode 100644
index 0000000..5fdba61
--- /dev/null
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/JavacCompilationTestRunner.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.util.runner
+
+import androidx.room.compiler.processing.SyntheticJavacProcessor
+import androidx.room.compiler.processing.util.CompilationResult
+import androidx.room.compiler.processing.util.JavaCompileTestingCompilationResult
+import androidx.room.compiler.processing.util.Source
+import com.google.testing.compile.Compiler
+
+internal object JavacCompilationTestRunner : CompilationTestRunner {
+
+ override val name: String = "javac"
+
+ override fun canRun(params: TestCompilationParameters): Boolean {
+ return params.sources.all { it is Source.JavaSource }
+ }
+
+ override fun compile(params: TestCompilationParameters): CompilationResult {
+ val syntheticJavacProcessor = SyntheticJavacProcessor(params.handler)
+ val sources = if (params.sources.isEmpty()) {
+ // synthesize a source to trigger compilation
+ listOf(
+ Source.java(
+ qName = "foo.bar.SyntheticSource",
+ code = """
+ package foo.bar;
+ public class SyntheticSource {}
+ """.trimIndent()
+ )
+ )
+ } else {
+ params.sources
+ }
+ val compiler = Compiler
+ .javac()
+ .withProcessors(syntheticJavacProcessor)
+ .withOptions("-Xlint")
+ .let {
+ if (params.classpath.isNotEmpty()) {
+ it.withClasspath(params.classpath)
+ } else {
+ it
+ }
+ }
+ val javaFileObjects = sources.map {
+ it.toJFO()
+ }
+ val compilation = compiler.compile(javaFileObjects)
+ return JavaCompileTestingCompilationResult(
+ testRunner = this,
+ delegate = compilation,
+ processor = syntheticJavacProcessor
+ )
+ }
+}
\ No newline at end of file
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KaptCompilationTestRunner.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KaptCompilationTestRunner.kt
new file mode 100644
index 0000000..e6cb5ee
--- /dev/null
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KaptCompilationTestRunner.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing.util.runner
+
+import androidx.room.compiler.processing.SyntheticJavacProcessor
+import androidx.room.compiler.processing.util.CompilationResult
+import androidx.room.compiler.processing.util.KotlinCompileTestingCompilationResult
+import com.tschuchort.compiletesting.KotlinCompilation
+
+internal object KaptCompilationTestRunner : CompilationTestRunner {
+
+ override val name: String = "kapt"
+
+ override fun canRun(params: TestCompilationParameters): Boolean {
+ return true
+ }
+
+ override fun compile(params: TestCompilationParameters): CompilationResult {
+ val syntheticJavacProcessor = SyntheticJavacProcessor(params.handler)
+ val compilation = KotlinCompilation()
+ params.sources.forEach {
+ compilation.workingDir.resolve("sources")
+ .resolve(it.relativePath())
+ .parentFile
+ .mkdirs()
+ }
+ compilation.sources = params.sources.map {
+ it.toKotlinSourceFile()
+ }
+ compilation.annotationProcessors = listOf(syntheticJavacProcessor)
+ compilation.inheritClassPath = true
+ compilation.verbose = false
+ compilation.classpaths += params.classpath
+
+ val result = compilation.compile()
+ return KotlinCompileTestingCompilationResult(
+ testRunner = this,
+ delegate = result,
+ processor = syntheticJavacProcessor,
+ successfulCompilation = result.exitCode == KotlinCompilation.ExitCode.OK
+ )
+ }
+}
\ No newline at end of file
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt
new file mode 100644
index 0000000..8ac5ed3
--- /dev/null
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/runner/KspCompilationTestRunner.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.util.runner
+
+import androidx.room.compiler.processing.SyntheticKspProcessor
+import androidx.room.compiler.processing.util.CompilationResult
+import androidx.room.compiler.processing.util.KotlinCompileTestingCompilationResult
+import androidx.room.compiler.processing.util.Source
+import com.tschuchort.compiletesting.KotlinCompilation
+import com.tschuchort.compiletesting.SourceFile
+import com.tschuchort.compiletesting.kspSourcesDir
+import com.tschuchort.compiletesting.symbolProcessors
+import java.io.File
+import javax.tools.Diagnostic
+
+internal object KspCompilationTestRunner : CompilationTestRunner {
+
+ override val name: String = "ksp"
+
+ override fun canRun(params: TestCompilationParameters): Boolean {
+ return true
+ }
+
+ override fun compile(params: TestCompilationParameters): CompilationResult {
+ @Suppress("NAME_SHADOWING")
+ val sources = if (params.sources.none { it is Source.KotlinSource }) {
+ // looks like this requires a kotlin source file
+ // see: https://github.com/tschuchortdev/kotlin-compile-testing/issues/57
+ params.sources + Source.kotlin("placeholder.kt", "")
+ } else {
+ params.sources
+ }
+ val syntheticKspProcessor = SyntheticKspProcessor(params.handler)
+ fun prepareCompilation(): KotlinCompilation {
+ val compilation = KotlinCompilation()
+ sources.forEach {
+ compilation.workingDir.resolve("sources")
+ .resolve(it.relativePath())
+ .parentFile
+ .mkdirs()
+ }
+ compilation.sources = sources.map {
+ it.toKotlinSourceFile()
+ }
+ compilation.jvmDefault = "enable"
+ compilation.jvmTarget = "1.8"
+ compilation.inheritClassPath = true
+ compilation.verbose = false
+ compilation.classpaths += params.classpath
+ return compilation
+ }
+
+ val kspCompilation = prepareCompilation()
+ kspCompilation.symbolProcessors = listOf(syntheticKspProcessor)
+ kspCompilation.compile()
+ // ignore KSP result for now because KSP stops compilation, which might create false
+ // negatives when java code accesses kotlin code.
+ // TODO: fix once https://github.com/tschuchortdev/kotlin-compile-testing/issues/72 is
+ // fixed
+
+ // after ksp, compile without ksp with KSP's output as input
+ val finalCompilation = prepareCompilation()
+ // build source files from generated code
+ finalCompilation.sources += kspCompilation.kspJavaSourceDir.collectSourceFiles() +
+ kspCompilation.kspKotlinSourceDir.collectSourceFiles()
+ val result = finalCompilation.compile()
+ // workaround for: https://github.com/google/ksp/issues/122
+ // KSP does not fail compilation for error diagnostics hence we do it here.
+ val hasErrorDiagnostics = syntheticKspProcessor.messageWatcher
+ .diagnostics()[Diagnostic.Kind.ERROR].orEmpty().isNotEmpty()
+ return KotlinCompileTestingCompilationResult(
+ testRunner = this,
+ delegate = result,
+ processor = syntheticKspProcessor,
+ successfulCompilation = result.exitCode == KotlinCompilation.ExitCode.OK &&
+ !hasErrorDiagnostics
+
+ )
+ }
+
+ // TODO get rid of these once kotlin compile testing supports two step compilation for KSP.
+ // https://github.com/tschuchortdev/kotlin-compile-testing/issues/72
+ private val KotlinCompilation.kspJavaSourceDir: File
+ get() = kspSourcesDir.resolve("java")
+
+ private val KotlinCompilation.kspKotlinSourceDir: File
+ get() = kspSourcesDir.resolve("kotlin")
+
+ private fun File.collectSourceFiles(): List<SourceFile> {
+ return walkTopDown().filter {
+ it.isFile
+ }.map { file ->
+ SourceFile.fromPath(file)
+ }.toList()
+ }
+}
\ No newline at end of file
diff --git a/room/compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/TestRunnerTest.kt b/room/compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/TestRunnerTest.kt
new file mode 100644
index 0000000..59dd47f7
--- /dev/null
+++ b/room/compiler-processing-testing/src/test/java/androidx/room/compiler/processing/util/TestRunnerTest.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.room.compiler.processing.util
+
+import com.squareup.javapoet.CodeBlock
+import com.squareup.javapoet.JavaFile
+import com.squareup.javapoet.TypeSpec
+import org.junit.Test
+import javax.tools.Diagnostic
+
+class TestRunnerTest {
+ @Test
+ fun generatedBadCode_expected() = generatedBadCode(assertFailure = true)
+
+ @Test(expected = AssertionError::class)
+ fun generatedBadCode_unexpected() = generatedBadCode(assertFailure = false)
+
+ private fun generatedBadCode(assertFailure: Boolean) {
+ runProcessorTest {
+ if (it.processingEnv.findTypeElement("foo.Foo") == null) {
+ val badCode = TypeSpec.classBuilder("Foo").apply {
+ addStaticBlock(
+ CodeBlock.of("bad code")
+ )
+ }.build()
+ val badGeneratedFile = JavaFile.builder("foo", badCode).build()
+ it.processingEnv.filer.write(
+ badGeneratedFile
+ )
+ }
+ if (assertFailure) {
+ it.assertCompilationResult {
+ compilationDidFail()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun reportedError_expected() = reportedError(assertFailure = true)
+
+ @Test(expected = AssertionError::class)
+ fun reportedError_unexpected() = reportedError(assertFailure = false)
+
+ fun reportedError(assertFailure: Boolean) {
+ runProcessorTest {
+ it.processingEnv.messager.printMessage(
+ kind = Diagnostic.Kind.ERROR,
+ msg = "reported error"
+ )
+ if (assertFailure) {
+ it.assertCompilationResult {
+ hasError("reported error")
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/compiler-processing/build.gradle b/room/compiler-processing/build.gradle
index 795a691..0562d8b 100644
--- a/room/compiler-processing/build.gradle
+++ b/room/compiler-processing/build.gradle
@@ -26,34 +26,22 @@
}
dependencies {
+ api(KOTLIN_STDLIB)
+ api(JAVAPOET)
implementation("androidx.annotation:annotation:1.1.0")
implementation(GUAVA)
- implementation(KOTLIN_STDLIB)
implementation(AUTO_COMMON)
implementation(AUTO_VALUE_ANNOTATIONS)
- implementation(JAVAPOET)
+
implementation(KOTLIN_METADATA_JVM)
implementation(INTELLIJ_ANNOTATIONS)
- implementation(KOTLIN_KSP_API) {
- version {
- // TODO remove after KSP versions are fixed
- // KSP 1.4 versions are not properly ordered due to rc vs dev versions (dev is latest
- // but gradle thinks rc is latest).
- // We have to enforce it to ensure the correct version is used.
- strictly KSP_VERSION
- }
- }
+ implementation(KOTLIN_KSP_API)
testImplementation(GOOGLE_COMPILE_TESTING)
testImplementation(JUNIT)
testImplementation(JSR250)
testImplementation(KOTLIN_COMPILE_TESTING_KSP)
- testImplementation(KOTLIN_KSP) {
- version {
- // TODO remove after KSP versions are fixed
- strictly KSP_VERSION
- }
- }
+ testImplementation(KOTLIN_KSP)
testImplementation(project(":room:room-compiler-processing-testing"))
}
diff --git a/room/compiler-processing/lint-baseline.xml b/room/compiler-processing/lint-baseline.xml
deleted file mode 100644
index 7a7fa1e..0000000
--- a/room/compiler-processing/lint-baseline.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-alpha15" client="gradle" version="4.2.0-alpha15">
-
- <issue
- id="BanUncheckedReflection"
- message="Calling Method.invoke without an SDK check"
- errorLine1=" return enumClass.getDeclaredMethod("valueOf", String::class.java)"
- errorLine2=" ^">
- <location
- file="src/main/java/androidx/room/compiler/processing/javac/JavacAnnotationBox.kt"
- line="260"
- column="20"/>
- </issue>
-
-</issues>
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XMessager.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XMessager.kt
index 0701c52..29ff818 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XMessager.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XMessager.kt
@@ -21,7 +21,8 @@
/**
* Logging interface for the processor
*/
-interface XMessager {
+abstract class XMessager {
+ private val watchers = mutableListOf<XMessager>()
/**
* Prints the given [msg] to the logs while also associating it with the given [element].
*
@@ -29,5 +30,20 @@
* @param msg The actual message to report to the compiler
* @param element The element with whom the message should be associated with
*/
- fun printMessage(kind: Diagnostic.Kind, msg: String, element: XElement? = null)
+ final fun printMessage(kind: Diagnostic.Kind, msg: String, element: XElement? = null) {
+ watchers.forEach {
+ it.printMessage(kind, msg, element)
+ }
+ onPrintMessage(kind, msg, element)
+ }
+
+ abstract fun onPrintMessage(kind: Diagnostic.Kind, msg: String, element: XElement? = null)
+
+ fun addMessageWatcher(watcher: XMessager) {
+ watchers.add(watcher)
+ }
+
+ fun removeMessageWatcher(watcher: XMessager) {
+ watchers.remove(watcher)
+ }
}
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacAnnotationBox.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacAnnotationBox.kt
index bff4938..227d6f6 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacAnnotationBox.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacAnnotationBox.kt
@@ -91,13 +91,20 @@
}
returnType.isArray && returnType.componentType.isAnnotation -> {
@Suppress("UNCHECKED_CAST")
- ListVisitor(env, returnType.componentType as Class<out Annotation>).visit(value)
+ AnnotationListVisitor(env, returnType.componentType as Class<out Annotation>)
+ .visit(value)
+ }
+ returnType.isArray && returnType.componentType.isEnum -> {
+ @Suppress("UNCHECKED_CAST")
+ EnumListVisitor(returnType.componentType as Class<out Enum<*>>).visit(value)
}
returnType.isEnum -> {
@Suppress("UNCHECKED_CAST")
value.getAsEnum(returnType as Class<out Enum<*>>)
}
- else -> throw UnsupportedOperationException("$returnType isn't supported")
+ else -> {
+ throw UnsupportedOperationException("$returnType isn't supported")
+ }
}
method.name to result
}
@@ -230,7 +237,7 @@
}
@Suppress("DEPRECATION")
-private class ListVisitor<T : Annotation>(
+private class AnnotationListVisitor<T : Annotation>(
private val env: JavacProcessingEnv,
private val annotationClass: Class<T>
) :
@@ -245,6 +252,24 @@
}
@Suppress("DEPRECATION")
+private class EnumListVisitor<T : Enum<T>>(private val enumClass: Class<T>) :
+ SimpleAnnotationValueVisitor6<Array<T>, Void?>() {
+ override fun visitArray(
+ values: MutableList<out AnnotationValue>?,
+ void: Void?
+ ): Array<T> {
+ val result = values?.map { it.getAsEnum(enumClass) }
+ @Suppress("UNCHECKED_CAST")
+ val resultArray = java.lang.reflect.Array
+ .newInstance(enumClass, result?.size ?: 0) as Array<T>
+ result?.forEachIndexed { index, value ->
+ resultArray[index] = value
+ }
+ return resultArray
+ }
+}
+
+@Suppress("DEPRECATION")
private class AnnotationClassVisitor<T : Annotation>(
private val env: JavacProcessingEnv,
private val annotationClass: Class<T>
@@ -253,7 +278,7 @@
override fun visitAnnotation(a: AnnotationMirror?, v: Void?) = a?.box(env, annotationClass)
}
-@Suppress("UNCHECKED_CAST", "DEPRECATION")
+@Suppress("UNCHECKED_CAST", "DEPRECATION", "BanUncheckedReflection")
private fun <T : Enum<*>> AnnotationValue.getAsEnum(enumClass: Class<T>): T {
return object : SimpleAnnotationValueVisitor6<T, Void>() {
override fun visitEnumConstant(value: VariableElement?, p: Void?): T {
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnvMessager.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnvMessager.kt
index f5120fa..e49a5f7 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnvMessager.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnvMessager.kt
@@ -27,8 +27,8 @@
internal class JavacProcessingEnvMessager(
private val processingEnv: ProcessingEnvironment
-) : XMessager {
- override fun printMessage(kind: Diagnostic.Kind, msg: String, element: XElement?) {
+) : XMessager() {
+ override fun onPrintMessage(kind: Diagnostic.Kind, msg: String, element: XElement?) {
val javacElement = (element as? JavacElement)?.element
processingEnv.messager.printMessage(
kind,
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotationBox.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotationBox.kt
index edfd964..b62e6ad 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotationBox.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotationBox.kt
@@ -19,6 +19,7 @@
import androidx.room.compiler.processing.XAnnotationBox
import androidx.room.compiler.processing.XType
import com.google.devtools.ksp.symbol.KSAnnotation
+import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSType
import java.lang.reflect.Proxy
@@ -29,7 +30,7 @@
private val annotation: KSAnnotation
) : XAnnotationBox<T> {
override fun getAsType(methodName: String): XType? {
- val value = getFieldValue<KSType>(methodName)
+ val value = getFieldValue(methodName, KSType::class.java)
return value?.let {
env.wrap(
ksType = it,
@@ -39,8 +40,8 @@
}
override fun getAsTypeList(methodName: String): List<XType> {
- val values = getFieldValue<List<KSType>>(methodName) ?: return emptyList()
- return values.map {
+ val values = getFieldValue(methodName, Array::class.java) ?: return emptyList()
+ return values.filterIsInstance<KSType>().map {
env.wrap(
ksType = it,
allowPrimitives = true
@@ -49,7 +50,17 @@
}
override fun <R : Annotation> getAsAnnotationBox(methodName: String): XAnnotationBox<R> {
- val value = getFieldValue<KSAnnotation>(methodName) ?: error("cannot get annotation")
+ val value = getFieldValue(methodName, KSAnnotation::class.java)
+ @Suppress("FoldInitializerAndIfToElvis")
+ if (value == null) {
+ // see https://github.com/google/ksp/issues/53
+ return KspReflectiveAnnotationBox.createFromDefaultValue(
+ env = env,
+ annotationClass = annotationClass,
+ methodName = methodName
+ )
+ }
+
val annotationType = annotationClass.methods.first {
it.name == methodName
}.returnType as Class<R>
@@ -60,22 +71,37 @@
)
}
- private inline fun <reified R> getFieldValue(methodName: String): R? {
- val value = annotation.arguments.firstOrNull {
+ @Suppress("SyntheticAccessor")
+ private fun <R : Any> getFieldValue(
+ methodName: String,
+ returnType: Class<R>
+ ): R? {
+ val methodValue = annotation.arguments.firstOrNull {
it.name?.asString() == methodName
- }?.value ?: return null
- return value as R?
+ }?.value
+ return methodValue?.readAs(returnType)
}
override fun <R : Annotation> getAsAnnotationBoxArray(
methodName: String
): Array<XAnnotationBox<R>> {
- val values = getFieldValue<ArrayList<*>>(methodName) ?: return emptyArray()
+ val values = getFieldValue(methodName, Array::class.java) ?: return emptyArray()
val annotationType = annotationClass.methods.first {
it.name == methodName
}.returnType.componentType as Class<R>
+ if (values.isEmpty()) {
+ // KSP is unable to read defaults and returns empty array in that case.
+ // Subsequently, we don't know if developer set it to empty array intentionally or
+ // left it to default.
+ // we error on the side of default
+ return KspReflectiveAnnotationBox.createFromDefaultValues(
+ env = env,
+ annotationClass = annotationClass,
+ methodName = methodName
+ )
+ }
return values.map {
- KspAnnotationBox<R>(
+ KspAnnotationBox(
env = env,
annotationClass = annotationType,
annotation = it as KSAnnotation
@@ -84,24 +110,52 @@
}
private val valueProxy: T = Proxy.newProxyInstance(
- KspAnnotationBox::class.java.classLoader,
+ annotationClass.classLoader,
arrayOf(annotationClass)
) { _, method, _ ->
- val fieldValue = getFieldValue(method.name) ?: method.defaultValue
- // java gives arrays, kotlin gives array list (sometimes?) so fix it up
- when {
- fieldValue == null -> null
- method.returnType.isArray && (fieldValue is ArrayList<*>) -> {
- val componentType = method.returnType.componentType!!
- val result =
- java.lang.reflect.Array.newInstance(componentType, fieldValue.size) as Array<*>
- fieldValue.toArray(result)
- result
- }
- else -> fieldValue
- }
+ getFieldValue(method.name, method.returnType) ?: method.defaultValue
} as T
override val value: T
get() = valueProxy
}
+
+@Suppress("UNCHECKED_CAST")
+private fun <R> Any.readAs(returnType: Class<R>): R? {
+ return when {
+ returnType.isArray -> {
+ val values = when (this) {
+ is List<*> -> {
+ // KSP might return list for arrays. convert it back.
+ this.mapNotNull {
+ it?.readAs(returnType.componentType)
+ }
+ }
+ is Array<*> -> mapNotNull { it?.readAs(returnType.componentType) }
+ else -> error("unexpected type for array: $this / ${this::class.java}")
+ }
+ val resultArray = java.lang.reflect.Array.newInstance(
+ returnType.componentType,
+ values.size
+ ) as Array<Any?>
+ values.forEachIndexed { index, value ->
+ resultArray[index] = value
+ }
+ resultArray
+ }
+ returnType.isEnum -> {
+ this.readAsEnum(returnType)
+ }
+ else -> this
+ } as R?
+}
+
+private fun <R> Any.readAsEnum(enumClass: Class<R>): R? {
+ val ksType = this as? KSType ?: return null
+ val classDeclaration = ksType.declaration as? KSClassDeclaration ?: return null
+ val enumValue = classDeclaration.simpleName.asString()
+ // get the instance from the valueOf function.
+ @Suppress("UNCHECKED_CAST", "BanUncheckedReflection")
+ return enumClass.getDeclaredMethod("valueOf", String::class.java)
+ .invoke(null, enumValue) as R?
+}
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMessager.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMessager.kt
index 557cfa3..1497d90 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMessager.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMessager.kt
@@ -23,8 +23,8 @@
internal class KspMessager(
private val logger: KSPLogger
-) : XMessager {
- override fun printMessage(kind: Diagnostic.Kind, msg: String, element: XElement?) {
+) : XMessager() {
+ override fun onPrintMessage(kind: Diagnostic.Kind, msg: String, element: XElement?) {
val ksNode = (element as? KspElement)?.declaration
when (kind) {
Diagnostic.Kind.ERROR -> logger.error(msg, ksNode)
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspReflectiveAnnotationBox.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspReflectiveAnnotationBox.kt
new file mode 100644
index 0000000..3bdd7ae
--- /dev/null
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspReflectiveAnnotationBox.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.ksp
+
+import androidx.annotation.VisibleForTesting
+import androidx.room.compiler.processing.XAnnotationBox
+import androidx.room.compiler.processing.XType
+
+/**
+ * KSP sometimes cannot read default values in annotations. This reflective implementation
+ * handles those cases.
+ * see: https://github.com/google/ksp/issues/53
+ */
+internal class KspReflectiveAnnotationBox<T : Annotation> @VisibleForTesting constructor(
+ private val env: KspProcessingEnv,
+ private val annotationClass: Class<T>,
+ private val annotation: T
+) : XAnnotationBox<T> {
+ override val value: T = annotation
+
+ override fun getAsType(methodName: String): XType? {
+ val value = getFieldValue<Class<*>>(methodName) ?: return null
+ return env.findType(value.kotlin)
+ }
+
+ override fun getAsTypeList(methodName: String): List<XType> {
+ val values = getFieldValue<Array<*>>(methodName)
+ return values?.filterIsInstance<Class<*>>()?.mapNotNull {
+ env.findType(it.kotlin)
+ } ?: emptyList()
+ }
+
+ override fun <T : Annotation> getAsAnnotationBox(methodName: String): XAnnotationBox<T> {
+ return createFromDefaultValue(
+ env = env,
+ annotationClass = annotationClass,
+ methodName = methodName
+ )
+ }
+
+ @Suppress("UNCHECKED_CAST", "BanUncheckedReflection")
+ override fun <T : Annotation> getAsAnnotationBoxArray(
+ methodName: String
+ ): Array<XAnnotationBox<T>> {
+ val method = annotationClass.methods.firstOrNull {
+ it.name == methodName
+ } ?: error("$annotationClass does not contain $methodName")
+ val values = method.invoke(annotation) as? Array<T> ?: return emptyArray()
+ return values.map {
+ KspReflectiveAnnotationBox(
+ env = env,
+ annotationClass = method.returnType.componentType as Class<T>,
+ annotation = it
+ )
+ }.toTypedArray()
+ }
+
+ @Suppress("UNCHECKED_CAST", "BanUncheckedReflection")
+ private fun <R : Any> getFieldValue(methodName: String): R? {
+ val value = annotationClass.methods.firstOrNull {
+ it.name == methodName
+ }?.invoke(annotation) ?: return null
+ return value as R?
+ }
+
+ companion object {
+ @Suppress("UNCHECKED_CAST")
+ fun <R : Annotation> createFromDefaultValue(
+ env: KspProcessingEnv,
+ annotationClass: Class<*>,
+ methodName: String
+ ): KspReflectiveAnnotationBox<R> {
+ val method = annotationClass.methods.firstOrNull {
+ it.name == methodName
+ } ?: error("$annotationClass does not contain $methodName")
+ val defaultValue = method.defaultValue
+ ?: error("$annotationClass.$method does not have a default value and is not set")
+ return KspReflectiveAnnotationBox(
+ env = env,
+ annotationClass = method.returnType as Class<R>,
+ annotation = defaultValue as R
+ )
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ fun <R : Annotation> createFromDefaultValues(
+ env: KspProcessingEnv,
+ annotationClass: Class<*>,
+ methodName: String
+ ): Array<XAnnotationBox<R>> {
+ val method = annotationClass.methods.firstOrNull {
+ it.name == methodName
+ } ?: error("$annotationClass does not contain $methodName")
+ check(method.returnType.isArray) {
+ "expected ${method.returnType} to be an array. $method"
+ }
+ val defaultValue = method.defaultValue
+ ?: error("$annotationClass.$method does not have a default value and is not set")
+ val values: Array<R> = defaultValue as Array<R>
+ return values.map {
+ KspReflectiveAnnotationBox(
+ env = env,
+ annotationClass = method.returnType.componentType as Class<R>,
+ annotation = it
+ )
+ }.toTypedArray()
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
index 60339a5..22993b2 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
@@ -317,4 +317,8 @@
.filter {
it.simpleName == this.simpleName
}
+
+ override fun toString(): String {
+ return declaration.toString()
+ }
}
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeMapper.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeMapper.kt
index 734f3b1..06a1983 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeMapper.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeMapper.kt
@@ -75,7 +75,7 @@
mapping["java.lang.Boolean"] = "kotlin.Boolean"
// collections. default to mutable ones since java types are always mutable
mapping["java.util.Iterator"] = "kotlin.collections.MutableIterator"
- mapping["java.util.Iterable"] = "kotlin.collections.Iterable"
+ mapping["java.lang.Iterable"] = "kotlin.collections.Iterable"
mapping["java.util.Collection"] = "kotlin.collections.MutableCollection"
mapping["java.util.Set"] = "kotlin.collections.MutableSet"
mapping["java.util.List"] = "kotlin.collections.MutableList"
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt
index e8a80d4..876eb4a 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt
@@ -25,7 +25,6 @@
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.KSPropertyAccessor
import com.google.devtools.ksp.symbol.KSPropertyDeclaration
-import com.google.devtools.ksp.symbol.Origin
internal fun Resolver.findClass(qName: String) = getClassDeclarationByName(
getKSNameFromString(qName)
@@ -92,12 +91,7 @@
}
private fun KSPropertyDeclaration.overrides(other: KSPropertyDeclaration): Boolean {
- val overridee = try {
- findOverridee()
- } catch (ex: NoSuchElementException) {
- // workaround for https://github.com/google/ksp/issues/174
- null
- }
+ val overridee = findOverridee()
if (overridee == other) {
return true
}
@@ -108,10 +102,6 @@
internal fun Resolver.safeGetJvmName(
declaration: KSFunctionDeclaration
): String {
- if (declaration.origin == Origin.JAVA) {
- // https://github.com/google/ksp/issues/170
- return declaration.simpleName.asString()
- }
return try {
getJvmName(declaration)
} catch (ignored: ClassCastException) {
@@ -126,10 +116,6 @@
accessor: KSPropertyAccessor,
fallback: () -> String
): String {
- if (accessor.origin == Origin.JAVA) {
- // https://github.com/google/ksp/issues/170
- return fallback()
- }
return try {
getJvmName(accessor)
} catch (ignored: ClassCastException) {
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
index 49ad742..b31aa76 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
@@ -22,7 +22,7 @@
import androidx.room.compiler.processing.util.XTestInvocation
import androidx.room.compiler.processing.util.javaTypeUtils
import androidx.room.compiler.processing.util.runKaptTest
-import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
+import androidx.room.compiler.processing.util.runProcessorTest
import com.google.auto.common.MoreTypes
import com.google.common.truth.Truth.assertThat
import com.squareup.javapoet.MethodSpec
@@ -291,7 +291,7 @@
private fun overridesCheck(source: Source, ignoreInheritedMethods: Boolean = false) {
// first build golden image with Java processor so we can use JavaPoet's API
val golden = buildMethodsViaJavaPoet(source, ignoreInheritedMethods)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
sources = listOf(source)
) { invocation ->
val (target, methods) = invocation.getOverrideTestTargets(ignoreInheritedMethods)
@@ -316,8 +316,7 @@
): List<String> {
lateinit var result: List<String>
runKaptTest(
- sources = listOf(source),
- succeed = true
+ sources = listOf(source)
) { invocation ->
val (target, methods) = invocation.getOverrideTestTargets(
ignoreInheritedMethods
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationBoxTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationBoxTest.kt
index 7a9a0ac..40cab24 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationBoxTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationBoxTest.kt
@@ -16,6 +16,8 @@
package androidx.room.compiler.processing
+import androidx.room.compiler.processing.testcode.JavaAnnotationWithDefaults
+import androidx.room.compiler.processing.testcode.JavaEnum
import androidx.room.compiler.processing.testcode.MainAnnotation
import androidx.room.compiler.processing.testcode.OtherAnnotation
import androidx.room.compiler.processing.util.Source
@@ -23,13 +25,15 @@
import androidx.room.compiler.processing.util.getMethod
import androidx.room.compiler.processing.util.getParameter
import androidx.room.compiler.processing.util.runProcessorTest
-import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
+import androidx.room.compiler.processing.util.runProcessorTestWithoutKsp
import androidx.room.compiler.processing.util.typeName
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import com.squareup.javapoet.ClassName
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import java.util.LinkedHashMap
@RunWith(JUnit4::class)
class XAnnotationBoxTest {
@@ -44,7 +48,6 @@
}
""".trimIndent()
)
- // TODO add KSP once https://github.com/google/ksp/issues/96 is fixed.
runProcessorTest(
sources = listOf(source)
) {
@@ -83,7 +86,8 @@
}
""".trimIndent()
)
- runProcessorTest(
+ // re-enable after fixing b/175144186
+ runProcessorTestWithoutKsp(
listOf(mySource)
) {
val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -125,7 +129,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
sources = listOf(source)
) {
val element = it.processingEnv.requireTypeElement("Subject")
@@ -165,7 +169,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
listOf(mySource)
) { invocation ->
val element = invocation.processingEnv.requireTypeElement("Subject")
@@ -225,7 +229,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+ runProcessorTest(sources = listOf(src)) { invocation ->
val subject = invocation.processingEnv.requireTypeElement("Subject")
subject.getField("prop1").assertHasSuppressWithValue("onProp1")
@@ -276,7 +280,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+ runProcessorTest(sources = listOf(src)) { invocation ->
val subject = invocation.processingEnv.requireTypeElement("Subject")
subject.getMethod("noAnnotations").let { method ->
method.assertDoesNotHaveAnnotation()
@@ -305,7 +309,7 @@
)
""".trimIndent()
)
- runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+ runProcessorTest(sources = listOf(src)) { invocation ->
val subject = invocation.processingEnv.requireTypeElement("Subject")
subject.assertHasSuppressWithValue("onClass")
val constructor = subject.getConstructors().single()
@@ -316,6 +320,76 @@
}
}
+ @Test
+ fun defaultValues() {
+ val kotlinSrc = Source.kotlin(
+ "KotlinClass.kt",
+ """
+ import androidx.room.compiler.processing.testcode.JavaAnnotationWithDefaults
+ @JavaAnnotationWithDefaults
+ class KotlinClass
+ """.trimIndent()
+ )
+ val javaSrc = Source.java(
+ "JavaClass.java",
+ """
+ import androidx.room.compiler.processing.testcode.JavaAnnotationWithDefaults;
+ @JavaAnnotationWithDefaults
+ class JavaClass {}
+ """.trimIndent()
+ )
+ runProcessorTest(sources = listOf(kotlinSrc, javaSrc)) { invocation ->
+ listOf("KotlinClass", "JavaClass")
+ .map {
+ invocation.processingEnv.requireTypeElement(it)
+ }.forEach { typeElement ->
+ val annotation =
+ typeElement.toAnnotationBox(JavaAnnotationWithDefaults::class)
+ checkNotNull(annotation)
+ assertThat(annotation.value.intVal).isEqualTo(3)
+ assertThat(annotation.value.stringArrayVal).isEqualTo(arrayOf("x", "y"))
+ assertThat(annotation.value.stringVal).isEqualTo("foo")
+ assertThat(
+ annotation.getAsType("typeVal")?.rawType?.typeName
+ ).isEqualTo(
+ ClassName.get(HashMap::class.java)
+ )
+ assertThat(
+ annotation.getAsTypeList("typeArrayVal").map {
+ it.rawType.typeName
+ }
+ ).isEqualTo(
+ listOf(ClassName.get(LinkedHashMap::class.java))
+ )
+
+ assertThat(
+ annotation.value.enumVal
+ ).isEqualTo(
+ JavaEnum.DEFAULT
+ )
+
+ assertThat(
+ annotation.value.enumArrayVal
+ ).isEqualTo(
+ arrayOf(JavaEnum.VAL1, JavaEnum.VAL2)
+ )
+
+ assertThat(
+ annotation.getAsAnnotationBox<OtherAnnotation>("otherAnnotationVal")
+ .value.value
+ ).isEqualTo("def")
+
+ assertThat(
+ annotation
+ .getAsAnnotationBoxArray<OtherAnnotation>("otherAnnotationArrayVal")
+ .map {
+ it.value.value
+ }
+ ).containsExactly("v1")
+ }
+ }
+ }
+
// helper function to read what we need
private fun XAnnotated.getSuppressValues(): Array<String>? {
return this.toAnnotationBox(SuppressWarnings::class)?.value?.value
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XArrayTypeTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XArrayTypeTest.kt
index 1f3260d..66b4de0 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XArrayTypeTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XArrayTypeTest.kt
@@ -23,7 +23,6 @@
import androidx.room.compiler.processing.util.kspResolver
import androidx.room.compiler.processing.util.runKspTest
import androidx.room.compiler.processing.util.runProcessorTest
-import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
import androidx.room.compiler.processing.util.typeName
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
@@ -43,7 +42,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
sources = listOf(source)
) { invocation ->
val type = invocation.processingEnv
@@ -63,7 +62,7 @@
@Test
fun synthetic() {
- runProcessorTestIncludingKsp {
+ runProcessorTest {
val objArray = it.processingEnv.getArrayType(
TypeName.OBJECT
)
@@ -89,7 +88,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
sources = listOf(source)
) { invocation ->
val element = invocation.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -140,7 +139,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(listOf(src)) {
+ runProcessorTest(listOf(src)) {
val subject = it.processingEnv.requireTypeElement("Subject")
val types = subject.getAllFieldsIncludingPrivateSupers().map {
assertWithMessage(it.name).that(it.type.isArray()).isTrue()
@@ -170,8 +169,7 @@
@Test
fun createArray() {
runKspTest(
- sources = emptyList(),
- succeed = true
+ sources = emptyList()
) { invocation ->
val intType = invocation.processingEnv.requireType("kotlin.Int")
invocation.processingEnv.getArrayType(intType).let {
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
index d84af6e..84623ee 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
@@ -22,8 +22,8 @@
import androidx.room.compiler.processing.util.getField
import androidx.room.compiler.processing.util.getMethod
import androidx.room.compiler.processing.util.getParameter
+import androidx.room.compiler.processing.util.runProcessorTestWithoutKsp
import androidx.room.compiler.processing.util.runProcessorTest
-import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
import com.google.common.truth.Truth.assertThat
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.TypeName
@@ -36,7 +36,7 @@
class XElementTest {
@Test
fun modifiers() {
- runProcessorTestIncludingKsp(
+ runProcessorTest(
listOf(
Source.java(
"foo.bar.Baz",
@@ -146,7 +146,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
listOf(genericBase, boundedChild)
) {
fun validateElement(element: XTypeElement, tTypeName: TypeName, rTypeName: TypeName) {
@@ -210,7 +210,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
listOf(source)
) {
val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -257,7 +257,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
listOf(source)
) {
val element = it.processingEnv.requireTypeElement("java.lang.Object")
@@ -280,7 +280,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
sources = listOf(subject)
) {
val inner = ClassName.get("foo.bar", "Baz.Inner")
@@ -317,7 +317,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
listOf(source)
) {
val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -350,7 +350,8 @@
}
""".trimIndent()
)
- runProcessorTest(
+ // enable once https://github.com/google/ksp/issues/167 is fixed
+ runProcessorTestWithoutKsp(
sources = listOf(source)
) {
val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -371,9 +372,14 @@
@Test
fun toStringMatchesUnderlyingElement() {
- runProcessorTest {
- it.processingEnv.findTypeElement("java.util.List").let { list ->
- assertThat(list.toString()).isEqualTo("java.util.List")
+ runProcessorTest { invocation ->
+ invocation.processingEnv.findTypeElement("java.util.List").let { list ->
+ val expected = if (invocation.isKsp) {
+ "MutableList"
+ } else {
+ "java.util.List"
+ }
+ assertThat(list.toString()).isEqualTo(expected)
}
}
}
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
index a270b74..d4c82e9 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
@@ -23,7 +23,7 @@
import androidx.room.compiler.processing.util.getDeclaredMethod
import androidx.room.compiler.processing.util.getMethod
import androidx.room.compiler.processing.util.getParameter
-import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
+import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.compiler.processing.util.typeName
import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
@@ -38,7 +38,7 @@
class XExecutableElementTest {
@Test
fun basic() {
- runProcessorTestIncludingKsp(
+ runProcessorTest(
sources = listOf(
Source.java(
"foo.bar.Baz",
@@ -89,7 +89,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
sources = listOf(subject)
) {
val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -108,7 +108,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
sources = listOf(subject)
) {
val element = it.processingEnv.requireTypeElement("Subject")
@@ -138,7 +138,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
sources = listOf(subject)
) {
val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -182,7 +182,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
sources = listOf(src)
) { invocation ->
val subject = invocation.processingEnv.requireTypeElement("Subject")
@@ -269,7 +269,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+ runProcessorTest(sources = listOf(src)) { invocation ->
val klass = invocation.processingEnv.requireTypeElement("MyDataClass")
val methodNames = klass.getAllMethods().map {
it.name
@@ -323,7 +323,7 @@
class NullableSubject: Base<String?>()
""".trimIndent()
)
- runProcessorTestIncludingKsp(sources = listOf(source)) { invocation ->
+ runProcessorTest(sources = listOf(source)) { invocation ->
val base = invocation.processingEnv.requireTypeElement("Base")
val subject = invocation.processingEnv.requireType("Subject")
.asDeclaredType()
@@ -386,7 +386,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(sources = listOf(src, javaSrc)) { invocation ->
+ runProcessorTest(sources = listOf(src, javaSrc)) { invocation ->
val base = invocation.processingEnv.requireTypeElement("MyInterface")
val impl = invocation.processingEnv.requireTypeElement("MyImpl")
val javaImpl = invocation.processingEnv.requireTypeElement("JavaImpl")
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt
index b2a03e5..38b007d 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableTypeTest.kt
@@ -20,7 +20,7 @@
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.UNIT_CLASS_NAME
import androidx.room.compiler.processing.util.getMethod
-import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
+import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.compiler.processing.util.typeName
import com.google.common.truth.Truth.assertThat
import com.squareup.javapoet.ParameterizedTypeName
@@ -43,7 +43,7 @@
abstract class Subject : MyInterface<String>
""".trimIndent()
)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
sources = listOf(src)
) { invocation ->
val myInterface = invocation.processingEnv.requireTypeElement("MyInterface")
@@ -112,7 +112,7 @@
abstract class NullableSubject: MyInterface<String?>
""".trimIndent()
)
- runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+ runProcessorTest(sources = listOf(src)) { invocation ->
val myInterface = invocation.processingEnv.requireTypeElement("MyInterface")
// helper method to get executable types both from sub class and also as direct child of
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
index 859e553..b808f2c 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
@@ -23,8 +23,8 @@
import androidx.room.compiler.processing.util.getField
import androidx.room.compiler.processing.util.getMethod
import androidx.room.compiler.processing.util.getParameter
+import androidx.room.compiler.processing.util.runProcessorTestWithoutKsp
import androidx.room.compiler.processing.util.runProcessorTest
-import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
import com.google.common.truth.Truth.assertThat
import com.squareup.javapoet.TypeName
import org.junit.Test
@@ -69,7 +69,7 @@
""".trimIndent()
)
// TODO run with KSP once https://github.com/google/ksp/issues/167 is fixed
- runProcessorTest(
+ runProcessorTestWithoutKsp(
sources = listOf(source)
) {
val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -164,7 +164,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
sources = listOf(source)
) {
val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -257,7 +257,7 @@
@Test
fun changeNullability_primitives() {
- runProcessorTestIncludingKsp { invocation ->
+ runProcessorTest { invocation ->
PRIMITIVE_TYPES.forEach { primitiveTypeName ->
val primitive = invocation.processingEnv.requireType(primitiveTypeName)
assertThat(primitive.nullability).isEqualTo(NONNULL)
@@ -295,7 +295,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(sources = listOf(javaSrc, kotlinSrc)) { invocation ->
+ runProcessorTest(sources = listOf(javaSrc, kotlinSrc)) { invocation ->
listOf("KotlinClass", "JavaClass").forEach {
val subject = invocation.processingEnv.requireTypeElement(it)
.getField("subject").type
@@ -316,7 +316,7 @@
@Test
fun changeNullability_declared() {
- runProcessorTestIncludingKsp { invocation ->
+ runProcessorTest { invocation ->
val subject = invocation.processingEnv.requireType("java.util.List")
subject.makeNullable().let {
assertThat(it.nullability).isEqualTo(NULLABLE)
@@ -337,7 +337,7 @@
@Test
fun changeNullability_arrayTypes() {
- runProcessorTestIncludingKsp { invocation ->
+ runProcessorTest { invocation ->
val subject = invocation.processingEnv.getArrayType(
invocation.processingEnv.requireType("java.util.List")
)
@@ -368,7 +368,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+ runProcessorTest(sources = listOf(src)) { invocation ->
val voidType = invocation.processingEnv.requireTypeElement("Foo")
.getMethod("subject").returnType
assertThat(voidType.typeName).isEqualTo(TypeName.VOID)
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingEnvTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingEnvTest.kt
index 4ae8c01..2f92301 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingEnvTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingEnvTest.kt
@@ -17,8 +17,7 @@
package androidx.room.compiler.processing
import androidx.room.compiler.processing.util.Source
-import androidx.room.compiler.processing.util.runProcessorTestForFailedCompilation
-import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
+import androidx.room.compiler.processing.util.runProcessorTest
import com.google.common.truth.Truth.assertThat
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.JavaFile
@@ -34,7 +33,7 @@
class XProcessingEnvTest {
@Test
fun getElement() {
- runProcessorTestIncludingKsp(
+ runProcessorTest(
listOf(
Source.java(
"foo.bar.Baz",
@@ -98,7 +97,7 @@
@Test
fun basic() {
- runProcessorTestIncludingKsp(
+ runProcessorTest(
listOf(
Source.java(
"foo.bar.Baz",
@@ -138,7 +137,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
listOf(source)
) { invocation ->
PRIMITIVE_TYPES.flatMap {
@@ -163,7 +162,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(sources = listOf(src)) {
+ runProcessorTest(sources = listOf(src)) {
it.processingEnv.requireTypeElement("foo.bar.Outer.Inner").let {
val className = it.className
assertThat(className.packageName()).isEqualTo("foo.bar")
@@ -175,7 +174,7 @@
@Test
fun findGeneratedAnnotation() {
- runProcessorTestIncludingKsp { invocation ->
+ runProcessorTest { invocation ->
val generatedAnnotation = invocation.processingEnv.findGeneratedAnnotation()
assertThat(generatedAnnotation?.name).isEqualTo("Generated")
}
@@ -200,7 +199,7 @@
""".trimIndent()
)
listOf(javaSrc, kotlinSrc).forEach { src ->
- runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+ runProcessorTest(sources = listOf(src)) { invocation ->
val className = ClassName.get("foo.bar", "ToBeGenerated")
if (invocation.processingEnv.findTypeElement(className) == null) {
// generate only if it doesn't exist to handle multi-round
@@ -223,14 +222,18 @@
class Foo {}
""".trimIndent()
)
- // TODO include KSP when https://github.com/google/ksp/issues/122 is fixed.
- runProcessorTestForFailedCompilation(
+ runProcessorTest(
sources = listOf(src)
) {
it.processingEnv.messager.printMessage(
Diagnostic.Kind.ERROR,
"intentional failure"
)
+ it.assertCompilationResult {
+ compilationDidFail()
+ .and()
+ .hasError("intentional failure")
+ }
}
}
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
index 2cd2588..4aa8ac9 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
@@ -24,9 +24,8 @@
import androidx.room.compiler.processing.util.javaElementUtils
import androidx.room.compiler.processing.util.kspResolver
import androidx.room.compiler.processing.util.runKspTest
+import androidx.room.compiler.processing.util.runProcessorTestWithoutKsp
import androidx.room.compiler.processing.util.runProcessorTest
-import androidx.room.compiler.processing.util.runProcessorTestForFailedCompilation
-import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
import androidx.room.compiler.processing.util.typeName
import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
@@ -53,7 +52,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
sources = listOf(parent)
) {
val type = it.processingEnv.requireType("foo.bar.Parent") as XDeclaredType
@@ -110,8 +109,9 @@
}
""".trimIndent()
)
- // TODO run with KSP as well once https://github.com/google/ksp/issues/107 is resolved
- runProcessorTestForFailedCompilation(
+
+ // enable KSP once https://github.com/google/ksp/issues/107 is fixed.
+ runProcessorTestWithoutKsp(
sources = listOf(missingTypeRef)
) {
val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -127,6 +127,9 @@
ClassName.get("", "NotExistingType")
)
}
+ it.assertCompilationResult {
+ compilationDidFail()
+ }
}
}
@@ -141,7 +144,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
sources = listOf(subject)
) {
val type = it.processingEnv.requireType("foo.bar.Baz")
@@ -174,7 +177,7 @@
@Test
fun isCollection_kotlin() {
- runKspTest(sources = emptyList(), succeed = true) { invocation ->
+ runKspTest(sources = emptyList()) { invocation ->
val subjects = listOf("Map" to false, "List" to true, "Set" to true)
subjects.forEach { (subject, expected) ->
invocation.processingEnv.requireType("kotlin.collections.$subject").let { type ->
@@ -187,7 +190,7 @@
@Test
fun toStringMatchesUnderlyingElement() {
- runProcessorTestIncludingKsp {
+ runProcessorTest {
val subject = "java.lang.String"
val expected = if (it.isKsp) {
it.kspResolver.getClassDeclarationByName(subject)?.toString()
@@ -212,12 +215,14 @@
}
""".trimIndent()
)
- // TODO run with KSP as well once https://github.com/google/ksp/issues/107 is resolved
- runProcessorTestForFailedCompilation(
+ runProcessorTest(
sources = listOf(missingTypeRef)
) {
val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
assertThat(element.superType?.isError()).isTrue()
+ it.assertCompilationResult {
+ compilationDidFail()
+ }
}
}
@@ -256,7 +261,7 @@
@Test
fun rawType() {
- runProcessorTestIncludingKsp {
+ runProcessorTest {
val subject = it.processingEnv.getDeclaredType(
it.processingEnv.requireTypeElement(List::class),
it.processingEnv.requireType(String::class)
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElementTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElementTest.kt
index 679af1c..7251136 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElementTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/javac/kotlin/KotlinMetadataElementTest.kt
@@ -18,7 +18,7 @@
import androidx.room.compiler.processing.javac.JavacProcessingEnv
import androidx.room.compiler.processing.util.Source
-import androidx.room.compiler.processing.util.runProcessorTest
+import androidx.room.compiler.processing.util.runKaptTest
import com.google.common.truth.Truth.assertThat
import org.junit.AssumptionViolatedException
import org.junit.Test
@@ -478,7 +478,7 @@
sources: List<Source> = emptyList(),
handler: (ProcessingEnvironment) -> Unit
) {
- runProcessorTest(sources) {
+ runKaptTest(sources) {
val processingEnv = it.processingEnv
if (processingEnv !is JavacProcessingEnv) {
throw AssumptionViolatedException("This test only works for java/kapt compilation")
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSAsMemberOfTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSAsMemberOfTest.kt
index 1bc7787..5cfa2d6 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSAsMemberOfTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSAsMemberOfTest.kt
@@ -48,7 +48,7 @@
""".trimIndent()
)
- runKspTest(sources = listOf(src), succeed = true) { invocation ->
+ runKspTest(sources = listOf(src)) { invocation ->
val base = invocation.processingEnv.requireTypeElement("BaseClass")
val sub = invocation.processingEnv.requireType("SubClass").asDeclaredType()
base.getField("normalInt").let { prop ->
@@ -121,7 +121,7 @@
abstract class NullableSubject: MyInterface<String?>()
""".trimIndent()
)
- runKspTest(sources = listOf(src), succeed = true) { invocation ->
+ runKspTest(sources = listOf(src)) { invocation ->
val myInterface = invocation.processingEnv.requireTypeElement("MyInterface")
val nonNullSubject = invocation.processingEnv.requireType("NonNullSubject")
.asDeclaredType()
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSTypeExtTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSTypeExtTest.kt
index c185f47..2865715 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSTypeExtTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSTypeExtTest.kt
@@ -26,7 +26,6 @@
import com.google.common.truth.Truth.assertThat
import com.google.devtools.ksp.getDeclaredFunctions
import com.google.devtools.ksp.getDeclaredProperties
-import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.ParameterizedTypeName
@@ -55,18 +54,18 @@
}
""".trimIndent()
)
- runTest(subjectSrc) { resolver ->
- val subject = resolver.requireClass("foo.bar.Baz")
- assertThat(subject.propertyType("intField").typeName(resolver))
+ runKspTest(sources = listOf(subjectSrc)) { invocation ->
+ val subject = invocation.kspResolver.requireClass("foo.bar.Baz")
+ assertThat(subject.propertyType("intField").typeName(invocation.kspResolver))
.isEqualTo(TypeName.INT)
- assertThat(subject.propertyType("listOfInts").typeName(resolver))
+ assertThat(subject.propertyType("listOfInts").typeName(invocation.kspResolver))
.isEqualTo(
ParameterizedTypeName.get(
List::class.className(),
TypeName.INT.box()
)
)
- assertThat(subject.propertyType("mutableMapOfAny").typeName(resolver))
+ assertThat(subject.propertyType("mutableMapOfAny").typeName(invocation.kspResolver))
.isEqualTo(
ParameterizedTypeName.get(
Map::class.className(),
@@ -74,7 +73,7 @@
TypeName.OBJECT,
)
)
- val typeName = subject.propertyType("nested").typeName(resolver)
+ val typeName = subject.propertyType("nested").typeName(invocation.kspResolver)
check(typeName is ClassName)
assertThat(typeName.packageName()).isEqualTo("foo.bar")
assertThat(typeName.simpleNames()).containsExactly("Baz", "Nested")
@@ -97,22 +96,25 @@
}
""".trimIndent()
)
- runTest(subjectSrc) { resolver ->
- val subject = resolver.requireClass("Baz")
- assertThat(subject.propertyType("intField").typeName(resolver))
- .isEqualTo(TypeName.INT)
- assertThat(subject.propertyType("listOfInts").typeName(resolver))
- .isEqualTo(
- ParameterizedTypeName.get(
- List::class.className(),
- TypeName.INT.box()
- )
+ runKspTest(sources = listOf(subjectSrc)) { invocation ->
+ val subject = invocation.kspResolver.requireClass("Baz")
+ assertThat(
+ subject.propertyType("intField").typeName(invocation.kspResolver)
+ ).isEqualTo(TypeName.INT)
+ assertThat(
+ subject.propertyType("listOfInts").typeName(invocation.kspResolver)
+ ).isEqualTo(
+ ParameterizedTypeName.get(
+ List::class.className(),
+ TypeName.INT.box()
)
- assertThat(subject.propertyType("incompleteGeneric").typeName(resolver))
- .isEqualTo(
- List::class.className()
- )
- assertThat(subject.propertyType("nested").typeName(resolver))
+ )
+ assertThat(
+ subject.propertyType("incompleteGeneric").typeName(invocation.kspResolver)
+ ).isEqualTo(
+ List::class.className()
+ )
+ assertThat(subject.propertyType("nested").typeName(invocation.kspResolver))
.isEqualTo(
ClassName.get("", "Baz", "Nested")
)
@@ -132,25 +134,31 @@
}
""".trimIndent()
)
- runTest(subjectSrc, succeed = false) { resolver ->
- val subject = resolver.requireClass("Foo")
- assertThat(subject.propertyType("errorField").typeName(resolver))
- .isEqualTo(ERROR_TYPE_NAME)
- assertThat(subject.propertyType("listOfError").typeName(resolver))
- .isEqualTo(
- ParameterizedTypeName.get(
- List::class.className(),
- ERROR_TYPE_NAME
- )
+ runKspTest(sources = listOf(subjectSrc)) { invocation ->
+ val subject = invocation.kspResolver.requireClass("Foo")
+ assertThat(
+ subject.propertyType("errorField").typeName(invocation.kspResolver)
+ ).isEqualTo(ERROR_TYPE_NAME)
+ assertThat(
+ subject.propertyType("listOfError").typeName(invocation.kspResolver)
+ ).isEqualTo(
+ ParameterizedTypeName.get(
+ List::class.className(),
+ ERROR_TYPE_NAME
)
- assertThat(subject.propertyType("mutableMapOfDontExist").typeName(resolver))
- .isEqualTo(
- ParameterizedTypeName.get(
- Map::class.className(),
- String::class.className(),
- ERROR_TYPE_NAME
- )
+ )
+ assertThat(
+ subject.propertyType("mutableMapOfDontExist").typeName(invocation.kspResolver)
+ ).isEqualTo(
+ ParameterizedTypeName.get(
+ Map::class.className(),
+ String::class.className(),
+ ERROR_TYPE_NAME
)
+ )
+ invocation.assertCompilationResult {
+ compilationDidFail()
+ }
}
}
@@ -181,8 +189,7 @@
// methodName -> returnType, ...paramTypes
val golden = mutableMapOf<String, List<TypeName>>()
runKaptTest(
- sources = listOf(src),
- succeed = true
+ sources = listOf(src)
) { invocation ->
val env = (invocation.processingEnv as JavacProcessingEnv)
val subject = env.delegate.elementUtils.getTypeElement("Subject")
@@ -196,8 +203,7 @@
}
val kspResults = mutableMapOf<String, List<TypeName>>()
runKspTest(
- sources = listOf(src),
- succeed = true
+ sources = listOf(src)
) { invocation ->
val env = (invocation.processingEnv as KspProcessingEnv)
val subject = env.resolver.requireClass("Subject")
@@ -220,21 +226,6 @@
assertThat(kspResults).containsExactlyEntriesIn(golden)
}
- private fun runTest(
- vararg sources: Source,
- succeed: Boolean = true,
- handler: (Resolver) -> Unit
- ) {
- runKspTest(
- sources = sources.toList(),
- succeed = succeed
- ) {
- handler(
- (it.processingEnv as KspProcessingEnv).resolver
- )
- }
- }
-
private fun KSClassDeclaration.requireProperty(name: String) = getDeclaredProperties().first {
it.simpleName.asString() == name
}
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFieldElementTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFieldElementTest.kt
index 1e8a5a2..f6666c4 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFieldElementTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFieldElementTest.kt
@@ -25,7 +25,7 @@
import androidx.room.compiler.processing.util.className
import androidx.room.compiler.processing.util.compileFiles
import androidx.room.compiler.processing.util.getField
-import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
+import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.compiler.processing.util.typeName
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
@@ -152,7 +152,7 @@
class Sub1 : Base<Int, String>()
""".trimIndent()
)
- runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+ runProcessorTest(sources = listOf(src)) { invocation ->
val sub = invocation.processingEnv.requireTypeElement("Sub1")
val base = invocation.processingEnv.requireTypeElement("Base")
val t = base.getField("t")
@@ -197,13 +197,13 @@
private fun runModifierTest(vararg inputs: ModifierTestInput) {
// we'll run the test twice. once it is in source and once it is coming from a dependency.
val sources = inputs.map(ModifierTestInput::source)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
sources = sources
) { invocation ->
assertModifiers(invocation, inputs)
}
val classpath = compileFiles(sources)
- runProcessorTestIncludingKsp(
+ runProcessorTest(
sources = emptyList(),
classpath = listOf(classpath)
) { invocation ->
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspReflectiveAnnotationBoxTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspReflectiveAnnotationBoxTest.kt
new file mode 100644
index 0000000..8c694265
--- /dev/null
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspReflectiveAnnotationBoxTest.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.ksp
+
+import androidx.room.compiler.processing.util.runKspTest
+import com.google.common.truth.Truth.assertThat
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.TypeName
+import org.junit.Test
+import kotlin.reflect.KClass
+
+class KspReflectiveAnnotationBoxTest {
+ enum class TestEnum {
+ VAL1,
+ VAL2
+ }
+
+ annotation class TestAnnotation(
+ val strProp: String = "abc",
+ val intProp: Int = 3,
+ val enumProp: TestEnum = TestEnum.VAL2,
+ val enumArrayProp: Array<TestEnum> = [TestEnum.VAL1, TestEnum.VAL2, TestEnum.VAL1],
+ val annProp: TestAnnotation2 = TestAnnotation2(3),
+ val annArrayProp: Array<TestAnnotation2> = [TestAnnotation2(1), TestAnnotation2(5)],
+ val typeProp: KClass<*> = Int::class,
+ val typeArrayProp: Array<KClass<*>> = [Int::class, String::class]
+ )
+
+ annotation class TestAnnotation2(
+ val intProp: Int = 0
+ )
+
+ @Test
+ @TestAnnotation // putting annotation here to read it back easily :)
+ fun simple() {
+ runKspTest(sources = emptyList()) { invocation ->
+ val box = KspReflectiveAnnotationBox(
+ env = invocation.processingEnv as KspProcessingEnv,
+ annotationClass = TestAnnotation::class.java,
+ annotation = getAnnotationOnMethod("simple")
+ )
+ assertThat(box.value.strProp).isEqualTo("abc")
+ assertThat(box.value.intProp).isEqualTo(3)
+ assertThat(box.value.enumProp).isEqualTo(TestEnum.VAL2)
+ assertThat(box.value.enumArrayProp).isEqualTo(
+ arrayOf(TestEnum.VAL1, TestEnum.VAL2, TestEnum.VAL1)
+ )
+ box.getAsAnnotationBox<TestAnnotation2>("annProp").let {
+ assertThat(it.value.intProp).isEqualTo(3)
+ }
+ box.getAsAnnotationBoxArray<TestAnnotation2>("annArrayProp").let {
+ assertThat(
+ it.map { it.value.intProp }
+ ).containsExactly(1, 5)
+ }
+ box.getAsType("typeProp")?.let {
+ assertThat(it is KspType).isTrue()
+ assertThat(it.typeName).isEqualTo(TypeName.INT)
+ }
+ box.getAsTypeList("typeArrayProp").let {
+ assertThat(it.all { it is KspType }).isTrue()
+ assertThat(it.map { it.typeName }).containsExactly(
+ TypeName.INT, ClassName.get(String::class.java)
+ )
+ }
+ }
+ }
+
+ private inline fun <reified T : Annotation> getAnnotationOnMethod(methodName: String): T {
+ return KspReflectiveAnnotationBoxTest::class.java.getMethod(methodName).annotations
+ .first {
+ it is TestAnnotation
+ } as T
+ }
+}
\ No newline at end of file
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeElementTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeElementTest.kt
index 437bdc3..804df7b 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeElementTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeElementTest.kt
@@ -23,7 +23,7 @@
import androidx.room.compiler.processing.util.getField
import androidx.room.compiler.processing.util.getMethod
import androidx.room.compiler.processing.util.runKspTest
-import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
+import androidx.room.compiler.processing.util.runProcessorTest
import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
import com.squareup.javapoet.ClassName
@@ -52,8 +52,7 @@
""".trimIndent()
)
runKspTest(
- sources = listOf(src1, src2),
- succeed = true
+ sources = listOf(src1, src2)
) { invocation ->
invocation.processingEnv.requireTypeElement("TopLevel").let {
assertThat(it.packageName).isEqualTo("")
@@ -93,7 +92,7 @@
interface MyInterface {}
""".trimIndent()
)
- runKspTest(sources = listOf(src), succeed = true) { invocation ->
+ runKspTest(sources = listOf(src)) { invocation ->
invocation.processingEnv.requireTypeElement("foo.bar.Baz").let {
assertThat(it.superType).isEqualTo(
invocation.processingEnv.requireType("foo.bar.AbstractClass")
@@ -134,7 +133,7 @@
}
""".trimIndent()
)
- runKspTest(sources = listOf(src), succeed = true) { invocation ->
+ runKspTest(sources = listOf(src)) { invocation ->
invocation.processingEnv.requireTypeElement("foo.bar.Outer").let {
assertThat(it.className).isEqualTo(ClassName.get("foo.bar", "Outer"))
assertThat(it.enclosingTypeElement).isNull()
@@ -163,7 +162,7 @@
private class PrivateClass
""".trimIndent()
)
- runKspTest(sources = listOf(src), succeed = true) { invocation ->
+ runKspTest(sources = listOf(src)) { invocation ->
fun getModifiers(element: XTypeElement): Set<String> {
val result = mutableSetOf<String>()
if (element.isAbstract()) result.add("abstract")
@@ -205,7 +204,7 @@
interface MyInterface
""".trimIndent()
)
- runKspTest(sources = listOf(src), succeed = true) { invocation ->
+ runKspTest(sources = listOf(src)) { invocation ->
invocation.processingEnv.requireTypeElement("MyClass").let {
assertThat(it.kindName()).isEqualTo("class")
}
@@ -228,7 +227,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+ runProcessorTest(sources = listOf(src)) { invocation ->
val baseClass = invocation.processingEnv.requireTypeElement("BaseClass")
assertThat(baseClass.getAllFieldNames()).containsExactly("genericProp")
val subClass = invocation.processingEnv.requireTypeElement("SubClass")
@@ -269,7 +268,7 @@
) : BaseClass(value)
""".trimIndent()
)
- runKspTest(sources = listOf(src), succeed = true) { invocation ->
+ runKspTest(sources = listOf(src)) { invocation ->
val baseClass = invocation.processingEnv.requireTypeElement("BaseClass")
assertThat(baseClass.getAllFieldNames()).containsExactly("value")
val subClass = invocation.processingEnv.requireTypeElement("SubClass")
@@ -317,7 +316,7 @@
}
""".trimIndent()
)
- runKspTest(sources = listOf(src), succeed = true) { invocation ->
+ runKspTest(sources = listOf(src)) { invocation ->
val base = invocation.processingEnv.requireTypeElement("Base")
assertThat(base.getDeclaredMethods().names()).containsExactly(
"baseFun", "suspendFun", "privateBaseFun", "staticBaseFun"
@@ -371,7 +370,7 @@
}
""".trimIndent()
)
- runKspTest(sources = listOf(src), succeed = true) { invocation ->
+ runKspTest(sources = listOf(src)) { invocation ->
val klass = invocation.processingEnv.requireTypeElement("SubClass")
assertThat(klass.getAllMethods().names()).containsExactly(
"baseMethod", "overriddenMethod", "baseCompanionMethod",
@@ -395,7 +394,7 @@
}
""".trimIndent()
)
- runKspTest(sources = listOf(src), succeed = true) { invocation ->
+ runKspTest(sources = listOf(src)) { invocation ->
invocation.processingEnv.requireTypeElement("JustGetter").let { base ->
assertThat(base.getDeclaredMethods().names()).containsExactly(
"getX"
@@ -438,7 +437,7 @@
class SubClass : CompanionSubject()
""".trimIndent()
)
- runKspTest(sources = listOf(src), succeed = true) { invocation ->
+ runKspTest(sources = listOf(src)) { invocation ->
val subject = invocation.processingEnv.requireTypeElement("CompanionSubject")
assertThat(subject.getAllFieldNames()).containsExactly(
"mutableStatic", "immutableStatic"
@@ -471,7 +470,7 @@
}
""".trimIndent()
)
- runKspTest(sources = listOf(src), succeed = true) { invocation ->
+ runKspTest(sources = listOf(src)) { invocation ->
invocation.processingEnv.requireTypeElement("JustGetter").let { base ->
assertThat(base.getDeclaredMethods().names()).containsExactly(
"getX"
@@ -520,7 +519,7 @@
abstract class AbstractExplicit(x:Int)
""".trimIndent()
)
- runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+ runProcessorTest(sources = listOf(src)) { invocation ->
val subjects = listOf(
"MyInterface", "NoExplicitConstructor", "Base", "ExplicitConstructor",
"BaseWithSecondary", "Sub", "SubWith3Constructors",
@@ -576,7 +575,7 @@
}
""".trimIndent()
)
- runKspTest(sources = listOf(src), succeed = true) { invocation ->
+ runKspTest(sources = listOf(src)) { invocation ->
val subject = invocation.processingEnv.requireTypeElement("MyInterface")
assertThat(subject.getMethod("notJvmDefault").isJavaDefault()).isFalse()
assertThat(subject.getMethod("jvmDefault").isJavaDefault()).isTrue()
@@ -623,7 +622,7 @@
}
""".trimIndent()
)
- runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+ runProcessorTest(sources = listOf(src)) { invocation ->
val subjects = listOf(
"MyInterface", "NoExplicitConstructor", "Base", "ExplicitConstructor",
"BaseWithSecondary", "Sub", "SubWith3Constructors",
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeTest.kt
index db0fc7dc..7643b04 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeTest.kt
@@ -50,7 +50,7 @@
interface MyInterface {}
""".trimIndent()
)
- runKspTest(listOf(src), succeed = true) {
+ runKspTest(listOf(src)) {
val subject = it.processingEnv.requireType("foo.bar.Baz")
assertThat(subject.typeName).isEqualTo(
ClassName.get("foo.bar", "Baz")
@@ -89,8 +89,7 @@
""".trimIndent()
)
runKspTest(
- listOf(src),
- succeed = false
+ listOf(src)
) { invocation ->
invocation.requireDeclaredPropertyType("errorType").let { type ->
assertThat(type.isError()).isTrue()
@@ -107,6 +106,9 @@
assertThat(typeArg.typeName).isEqualTo(ERROR_TYPE_NAME)
}
}
+ invocation.assertCompilationResult {
+ compilationDidFail()
+ }
}
}
@@ -121,8 +123,7 @@
""".trimIndent()
)
runKspTest(
- listOf(src),
- succeed = true
+ listOf(src)
) { invocation ->
invocation.requireDeclaredPropertyType("listOfNullableStrings").let { type ->
assertThat(type.nullability).isEqualTo(NONNULL)
@@ -173,8 +174,7 @@
""".trimIndent()
)
runKspTest(
- listOf(src),
- succeed = true
+ listOf(src)
) { invocation ->
val nullableStringList = invocation
.requireDeclaredPropertyType("listOfNullableStrings")
@@ -220,8 +220,7 @@
""".trimIndent()
)
runKspTest(
- listOf(src),
- succeed = true
+ listOf(src)
) { invocation ->
invocation.requirePropertyType("simple").let {
assertThat(it.rawType.typeName).isEqualTo(TypeName.INT)
@@ -257,7 +256,7 @@
}
""".trimIndent()
)
- runKspTest(sources = listOf(src), succeed = true) { invocation ->
+ runKspTest(sources = listOf(src)) { invocation ->
val resolver = (invocation.processingEnv as KspProcessingEnv).resolver
val voidMethod = resolver.getClassDeclarationByName("foo.bar.Baz")!!
.getDeclaredFunctions()
@@ -289,8 +288,7 @@
""".trimIndent()
)
runKspTest(
- listOf(src),
- succeed = false
+ listOf(src)
) { invocation ->
fun mapProp(name: String) = invocation.requirePropertyType(name).let {
listOf(
@@ -313,6 +311,9 @@
assertThat(mapProp("nullableByteProp")).containsExactly("isByte")
assertThat(mapProp("errorProp")).containsExactly("isError")
assertThat(mapProp("nullableErrorProp")).containsExactly("isError")
+ invocation.assertCompilationResult {
+ compilationDidFail()
+ }
}
}
@@ -334,8 +335,7 @@
""".trimIndent()
)
runKspTest(
- listOf(src),
- succeed = false
+ listOf(src)
) { invocation ->
fun getDefaultValue(name: String) = invocation.requirePropertyType(name).defaultValue()
// javac types do not check nullability but checking it is more correct
@@ -351,6 +351,9 @@
assertThat(getDefaultValue("errorProp")).isEqualTo("null")
assertThat(getDefaultValue("nullableErrorProp")).isEqualTo("null")
assertThat(getDefaultValue("stringProp")).isEqualTo("null")
+ invocation.assertCompilationResult {
+ compilationDidFail()
+ }
}
}
@@ -366,8 +369,7 @@
""".trimIndent()
)
runKspTest(
- listOf(src),
- succeed = true
+ listOf(src)
) { invocation ->
assertThat(
invocation.requirePropertyType("stringProp").isTypeOf(
@@ -418,8 +420,7 @@
""".trimIndent()
)
runKspTest(
- listOf(src),
- succeed = true
+ listOf(src)
) { invocation ->
fun check(prop1: String, prop2: String): Boolean {
return invocation.requirePropertyType(prop1).isSameType(
@@ -450,8 +451,7 @@
""".trimIndent()
)
runKspTest(
- listOf(src),
- succeed = true
+ listOf(src)
) { invocation ->
val env = (invocation.processingEnv as KspProcessingEnv)
val classNames = listOf("Bar", "Bar_NullableFoo")
@@ -491,8 +491,7 @@
""".trimIndent()
)
runKspTest(
- listOf(src),
- succeed = true
+ listOf(src)
) { invocation ->
val env = (invocation.processingEnv as KspProcessingEnv)
val method = env.resolver
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/testcode/JavaAnnotationWithDefaults.java b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/testcode/JavaAnnotationWithDefaults.java
new file mode 100644
index 0000000..26361dc
--- /dev/null
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/testcode/JavaAnnotationWithDefaults.java
@@ -0,0 +1,32 @@
+/*
+ * 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.testcode;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+
+public @interface JavaAnnotationWithDefaults {
+ String stringVal() default "foo";
+ String[] stringArrayVal() default {"x", "y"};
+ Class<?> typeVal() default HashMap.class;
+ Class[] typeArrayVal() default {LinkedHashMap.class};
+ int intVal() default 3;
+ JavaEnum enumVal() default JavaEnum.DEFAULT;
+ JavaEnum[] enumArrayVal() default {JavaEnum.VAL1, JavaEnum.VAL2};
+ OtherAnnotation otherAnnotationVal() default @OtherAnnotation("def");
+ OtherAnnotation[] otherAnnotationArrayVal() default {@OtherAnnotation("v1")};
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/testcode/JavaEnum.java
similarity index 79%
rename from compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
rename to room/compiler-processing/src/test/java/androidx/room/compiler/processing/testcode/JavaEnum.java
index f9cb2fe..c0a037b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/testcode/JavaEnum.java
@@ -14,7 +14,10 @@
* limitations under the License.
*/
-package androidx.compose.ui.focus
+package androidx.room.compiler.processing.testcode;
-@RequiresOptIn("The Focus API is experimental and is likely to change in the future.")
-annotation class ExperimentalFocus
\ No newline at end of file
+public enum JavaEnum {
+ VAL1,
+ VAL2,
+ DEFAULT
+}
diff --git a/room/compiler/build.gradle b/room/compiler/build.gradle
index fc529d1..6cca78d 100644
--- a/room/compiler/build.gradle
+++ b/room/compiler/build.gradle
@@ -114,6 +114,7 @@
implementation(INTELLIJ_ANNOTATIONS)
testImplementation(GOOGLE_COMPILE_TESTING)
testImplementation projectOrArtifact(":paging:paging-common")
+ testImplementation(project(":room:room-compiler-processing-testing"))
testImplementation(JUNIT)
testImplementation(JSR250)
testImplementation(MOCKITO_CORE)
diff --git a/room/compiler/src/main/kotlin/androidx/room/log/RLog.kt b/room/compiler/src/main/kotlin/androidx/room/log/RLog.kt
index d18778e..0ce7b73 100644
--- a/room/compiler/src/main/kotlin/androidx/room/log/RLog.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/log/RLog.kt
@@ -75,9 +75,9 @@
messager.printMessage(WARNING, msg.safeFormat(args), defaultElement)
}
- class CollectingMessager : XMessager {
+ class CollectingMessager : XMessager() {
private val messages = mutableMapOf<Diagnostic.Kind, MutableList<Pair<String, XElement?>>>()
- override fun printMessage(kind: Diagnostic.Kind, msg: String, element: XElement?) {
+ override fun onPrintMessage(kind: Diagnostic.Kind, msg: String, element: XElement?) {
messages.getOrPut(
kind,
{
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedBooleanToBoxedIntConverter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedBooleanToBoxedIntConverter.kt
index 7a3664c..2e53e25 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedBooleanToBoxedIntConverter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedBooleanToBoxedIntConverter.kt
@@ -25,8 +25,8 @@
*/
object BoxedBooleanToBoxedIntConverter {
fun create(processingEnvironment: XProcessingEnv): List<TypeConverter> {
- val tBoolean = processingEnvironment.requireType("java.lang.Boolean")
- val tInt = processingEnvironment.requireType("java.lang.Integer")
+ val tBoolean = processingEnvironment.requireType("java.lang.Boolean").makeNullable()
+ val tInt = processingEnvironment.requireType("java.lang.Integer").makeNullable()
return listOf(
object : TypeConverter(tBoolean, tInt) {
override fun convert(
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedPrimitiveColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedPrimitiveColumnTypeAdapter.kt
index 6fc5cc5..e47d917 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedPrimitiveColumnTypeAdapter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/BoxedPrimitiveColumnTypeAdapter.kt
@@ -34,7 +34,7 @@
return primitiveAdapters.map {
BoxedPrimitiveColumnTypeAdapter(
- it.out.boxed(),
+ it.out.boxed().makeNullable(),
it
)
}
diff --git a/room/compiler/src/main/kotlin/androidx/room/util/SimpleJavaVersion.kt b/room/compiler/src/main/kotlin/androidx/room/util/SimpleJavaVersion.kt
index 8041f42..5655d9b 100644
--- a/room/compiler/src/main/kotlin/androidx/room/util/SimpleJavaVersion.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/util/SimpleJavaVersion.kt
@@ -23,8 +23,11 @@
* [androidx.room.RoomProcessor.methodParametersVisibleInClassFiles] check only. If you want to use
* this class, consider expanding the implementation or use a different library.
*/
-data class SimpleJavaVersion(val major: Int, val minor: Int, val update: Int? = null) :
- Comparable<SimpleJavaVersion> {
+data class SimpleJavaVersion(
+ val major: Int,
+ val minor: Int,
+ val update: Int? = null
+) : Comparable<SimpleJavaVersion> {
override fun compareTo(other: SimpleJavaVersion): Int {
return compareValuesBy(
@@ -59,13 +62,23 @@
val parts = version.split('.')
- // There are valid JDK version strings with more than 3 parts when split by dots.
- // For example: "11.0.6+10-post-Ubuntu-1ubuntu118.04.1".
- if (parts.size < 3) {
- return null
+ // There are valid JDK version strings with no parts split by dots.
+ // For example: "15+36".
+ if (parts.size == 1) {
+ return try {
+ val major = parts[0].substringBeforeNonDigitChar()
+ SimpleJavaVersion(major.toInt(), 0)
+ } catch (e: NumberFormatException) {
+ null
+ }
}
if (parts[0] == "1") {
+ // All 3 parts are needed when JDK versions strings where major version is 1.
+ // For example: "1.8.0_202-release-1483-b39-5396753"
+ if (parts.size < 3) {
+ return null
+ }
val major = parts[1]
val minorAndUpdate = parts[2].substringBefore('-').split('_')
if (minorAndUpdate.size != 2) {
@@ -82,13 +95,23 @@
}
} else {
return try {
- SimpleJavaVersion(parts[0].toInt(), parts[1].toInt())
+ val minor = parts[1].substringBeforeNonDigitChar()
+ SimpleJavaVersion(parts[0].toInt(), minor.toInt())
} catch (e: NumberFormatException) {
null
}
}
}
+ private fun String.substringBeforeNonDigitChar(): String {
+ val nonDigitIndex = indexOfFirst { !it.isDigit() }
+ return if (nonDigitIndex == -1) {
+ this
+ } else {
+ substring(0, nonDigitIndex)
+ }
+ }
+
/**
* Parses the Java version from the given string (e.g.,
* "1.8.0_202-release-1483-b39-5396753"), throwing [IllegalArgumentException] if it
diff --git a/room/compiler/src/test/kotlin/androidx/room/log/RLogTest.kt b/room/compiler/src/test/kotlin/androidx/room/log/RLogTest.kt
index b87099d..71a4ce0 100644
--- a/room/compiler/src/test/kotlin/androidx/room/log/RLogTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/log/RLogTest.kt
@@ -16,20 +16,22 @@
package androidx.room.log
+import androidx.room.compiler.processing.XElement
import androidx.room.compiler.processing.XMessager
import androidx.room.vo.Warning
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-import org.mockito.Mockito.mock
+import javax.tools.Diagnostic
@RunWith(JUnit4::class)
class RLogTest {
-
- val messager = mock(XMessager::class.java)
-
@Test
fun testSafeFormat() {
+ val messager = object : XMessager() {
+ override fun onPrintMessage(kind: Diagnostic.Kind, msg: String, element: XElement?) {
+ }
+ }
val logger = RLog(messager, emptySet(), null)
// UnknownFormatConversionException
diff --git a/room/compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt b/room/compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
index f7ad69b..ab48fa7a 100644
--- a/room/compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
@@ -19,9 +19,11 @@
import COMMON
import androidx.paging.DataSource
import androidx.paging.PagingSource
-import androidx.room.Entity
import androidx.room.compiler.processing.XProcessingEnv
import androidx.room.compiler.processing.asDeclaredType
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.ext.GuavaUtilConcurrentTypeNames
import androidx.room.ext.L
import androidx.room.ext.LifecyclesTypeNames
@@ -46,12 +48,7 @@
import androidx.room.solver.types.CompositeAdapter
import androidx.room.solver.types.EnumColumnTypeAdapter
import androidx.room.solver.types.TypeConverter
-import androidx.room.testing.TestInvocation
-import androidx.room.testing.TestProcessor
-import com.google.common.truth.Truth
-import com.google.testing.compile.CompileTester
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourcesSubjectFactory
+import androidx.room.testing.context
import com.squareup.javapoet.TypeName
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.CoreMatchers.instanceOf
@@ -61,8 +58,8 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-import simpleRun
import testCodeGenScope
+import toSources
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
@RunWith(JUnit4::class)
@@ -73,21 +70,24 @@
@Test
fun testDirect() {
- singleRun { invocation ->
+ runProcessorTest { invocation ->
val store = TypeAdapterStore.create(Context(invocation.processingEnv))
val primitiveType = invocation.processingEnv.requireType(TypeName.INT)
val adapter = store.findColumnTypeAdapter(primitiveType, null)
assertThat(adapter, notNullValue())
- }.compilesWithoutError()
+ }
}
@Test
fun testJavaLangBoolean() {
- singleRun { invocation ->
- val store = TypeAdapterStore.create(Context(invocation.processingEnv))
+ runProcessorTest { invocation ->
+ val store = TypeAdapterStore.create(
+ Context(invocation.processingEnv)
+ )
val boolean = invocation
.processingEnv
.requireType("java.lang.Boolean")
+ .makeNullable()
val adapter = store.findColumnTypeAdapter(boolean, null)
assertThat(adapter, notNullValue())
assertThat(adapter, instanceOf(CompositeAdapter::class.java))
@@ -100,22 +100,23 @@
composite.columnTypeAdapter.out.typeName,
`is`(TypeName.INT.box())
)
- }.compilesWithoutError()
+ }
}
@Test
fun testJavaLangEnumCompilesWithoutError() {
- simpleRun(
- JavaFileObjects.forSourceString(
- "foo.bar.Fruit",
- """ package foo.bar;
+ val enumSrc = Source.java(
+ "foo.bar.Fruit",
+ """ package foo.bar;
import androidx.room.*;
enum Fruit {
APPLE,
BANANA,
STRAWBERRY}
""".trimMargin()
- )
+ )
+ runProcessorTest(
+ sources = listOf(enumSrc)
) { invocation ->
val store = TypeAdapterStore.create(Context(invocation.processingEnv))
val enum = invocation
@@ -124,12 +125,12 @@
val adapter = store.findColumnTypeAdapter(enum, null)
assertThat(adapter, notNullValue())
assertThat(adapter, instanceOf(EnumColumnTypeAdapter::class.java))
- }.compilesWithoutError()
+ }
}
@Test
fun testVia1TypeAdapter() {
- singleRun { invocation ->
+ runProcessorTest { invocation ->
val store = TypeAdapterStore.create(Context(invocation.processingEnv))
val booleanType = invocation.processingEnv.requireType(TypeName.BOOLEAN)
val adapter = store.findColumnTypeAdapter(booleanType, null)
@@ -160,12 +161,35 @@
""".trimIndent()
)
)
- }.compilesWithoutError()
+ }
}
@Test
fun testVia2TypeAdapters() {
- singleRun { invocation ->
+ val point = Source.java(
+ "foo.bar.Point",
+ """
+ package foo.bar;
+ import androidx.room.*;
+ @Entity
+ public class Point {
+ public int x, y;
+ public Point(int x, int y) {
+ this.x = x;
+ this.y = y;
+ }
+ public static Point fromBoolean(boolean val) {
+ return val ? new Point(1, 1) : new Point(0, 0);
+ }
+ public static boolean toBoolean(Point point) {
+ return point.x > 0;
+ }
+ }
+ """
+ )
+ runProcessorTest(
+ sources = listOf(point)
+ ) { invocation ->
val store = TypeAdapterStore.create(
Context(invocation.processingEnv),
pointTypeConverters(invocation.processingEnv)
@@ -204,17 +228,17 @@
""".trimIndent()
)
)
- }.compilesWithoutError()
+ }
}
@Test
fun testDate() {
- singleRun { (processingEnv) ->
+ runProcessorTest { invocation ->
val store = TypeAdapterStore.create(
- Context(processingEnv),
- dateTypeConverters(processingEnv)
+ invocation.context,
+ dateTypeConverters(invocation.processingEnv)
)
- val tDate = processingEnv.requireType("java.util.Date")
+ val tDate = invocation.processingEnv.requireType("java.util.Date")
val adapter = store.findCursorValueReader(tDate, SQLTypeAffinity.INTEGER)
assertThat(adapter, notNullValue())
assertThat(adapter?.typeMirror(), `is`(tDate))
@@ -234,12 +258,12 @@
""".trimIndent()
)
)
- }.compilesWithoutError()
+ }
}
@Test
fun testIntList() {
- singleRun { invocation ->
+ runProcessorTest { invocation ->
val binders = createIntListToStringBinders(invocation)
val store = TypeAdapterStore.create(
Context(invocation.processingEnv), binders[0],
@@ -272,12 +296,12 @@
)
assertThat(converter, notNullValue())
assertThat(store.reverse(converter!!), `is`(binders[1]))
- }.compilesWithoutError()
+ }
}
@Test
fun testOneWayConversion() {
- singleRun { invocation ->
+ runProcessorTest { invocation ->
val binders = createIntListToStringBinders(invocation)
val store = TypeAdapterStore.create(Context(invocation.processingEnv), binders[0])
val adapter = store.findColumnTypeAdapter(binders[0].from, null)
@@ -298,7 +322,9 @@
@Test
fun testMissingRx2Room() {
@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
- simpleRun(jfos = arrayOf(COMMON.PUBLISHER, COMMON.RX2_FLOWABLE)) { invocation ->
+ runProcessorTest(
+ sources = listOf(COMMON.PUBLISHER, COMMON.RX2_FLOWABLE).toSources()
+ ) { invocation ->
val publisherElement = invocation.processingEnv
.requireTypeElement(ReactiveStreamsTypeNames.PUBLISHER)
assertThat(publisherElement, notNullValue())
@@ -308,13 +334,18 @@
},
`is`(true)
)
- }.failsToCompile().withErrorContaining(ProcessorErrors.MISSING_ROOM_RXJAVA2_ARTIFACT)
+ invocation.assertCompilationResult {
+ hasError(ProcessorErrors.MISSING_ROOM_RXJAVA2_ARTIFACT)
+ }
+ }
}
@Test
fun testMissingRx3Room() {
@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
- simpleRun(jfos = arrayOf(COMMON.PUBLISHER, COMMON.RX3_FLOWABLE)) { invocation ->
+ runProcessorTest(
+ sources = listOf(COMMON.PUBLISHER, COMMON.RX3_FLOWABLE).toSources()
+ ) { invocation ->
val publisherElement = invocation.processingEnv
.requireTypeElement(ReactiveStreamsTypeNames.PUBLISHER)
assertThat(publisherElement, notNullValue())
@@ -324,7 +355,10 @@
},
`is`(true)
)
- }.failsToCompile().withErrorContaining(ProcessorErrors.MISSING_ROOM_RXJAVA3_ARTIFACT)
+ invocation.assertCompilationResult {
+ hasError(ProcessorErrors.MISSING_ROOM_RXJAVA3_ARTIFACT)
+ }
+ }
}
@Test
@@ -334,8 +368,9 @@
COMMON.RX3_FLOWABLE to COMMON.RX3_ROOM
).forEach { (rxTypeSrc, rxRoomSrc) ->
@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
- simpleRun(jfos = arrayOf(COMMON.PUBLISHER, rxTypeSrc, rxRoomSrc)) {
- invocation ->
+ runProcessorTest(
+ sources = listOf(COMMON.PUBLISHER, rxTypeSrc, rxRoomSrc).toSources()
+ ) { invocation ->
val publisher = invocation.processingEnv
.requireTypeElement(ReactiveStreamsTypeNames.PUBLISHER)
assertThat(publisher, notNullValue())
@@ -345,7 +380,7 @@
},
`is`(true)
)
- }.compilesWithoutError()
+ }
}
}
@@ -356,8 +391,9 @@
Triple(COMMON.RX3_FLOWABLE, COMMON.RX3_ROOM, RxJava3TypeNames.FLOWABLE)
).forEach { (rxTypeSrc, rxRoomSrc, rxTypeClassName) ->
@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
- simpleRun(jfos = arrayOf(COMMON.PUBLISHER, rxTypeSrc, rxRoomSrc)) {
- invocation ->
+ runProcessorTest(
+ sources = listOf(COMMON.PUBLISHER, rxTypeSrc, rxRoomSrc).toSources()
+ ) { invocation ->
val flowable = invocation.processingEnv.requireTypeElement(rxTypeClassName)
assertThat(
RxQueryResultBinderProvider.getAll(invocation.context).any {
@@ -365,7 +401,7 @@
},
`is`(true)
)
- }.compilesWithoutError()
+ }
}
}
@@ -376,8 +412,9 @@
Triple(COMMON.RX3_OBSERVABLE, COMMON.RX3_ROOM, RxJava3TypeNames.OBSERVABLE)
).forEach { (rxTypeSrc, rxRoomSrc, rxTypeClassName) ->
@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
- simpleRun(jfos = arrayOf(rxTypeSrc, rxRoomSrc)) {
- invocation ->
+ runProcessorTest(
+ sources = listOf(rxTypeSrc, rxRoomSrc).toSources()
+ ) { invocation ->
val observable = invocation.processingEnv.requireTypeElement(rxTypeClassName)
assertThat(observable, notNullValue())
assertThat(
@@ -386,7 +423,7 @@
},
`is`(true)
)
- }.compilesWithoutError()
+ }
}
}
@@ -397,8 +434,7 @@
Triple(COMMON.RX3_SINGLE, COMMON.RX3_ROOM, RxJava3TypeNames.SINGLE)
).forEach { (rxTypeSrc, _, rxTypeClassName) ->
@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
- simpleRun(jfos = arrayOf(rxTypeSrc)) {
- invocation ->
+ runProcessorTest(sources = listOf(rxTypeSrc).toSources()) { invocation ->
val single = invocation.processingEnv.requireTypeElement(rxTypeClassName)
assertThat(single, notNullValue())
assertThat(
@@ -407,7 +443,7 @@
},
`is`(true)
)
- }.compilesWithoutError()
+ }
}
}
@@ -417,9 +453,7 @@
Triple(COMMON.RX2_MAYBE, COMMON.RX2_ROOM, RxJava2TypeNames.MAYBE),
Triple(COMMON.RX3_MAYBE, COMMON.RX3_ROOM, RxJava3TypeNames.MAYBE)
).forEach { (rxTypeSrc, _, rxTypeClassName) ->
- @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
- simpleRun(jfos = arrayOf(rxTypeSrc)) {
- invocation ->
+ runProcessorTest(sources = listOf(rxTypeSrc).toSources()) { invocation ->
val maybe = invocation.processingEnv.requireTypeElement(rxTypeClassName)
assertThat(
RxCallableInsertMethodBinderProvider.getAll(invocation.context).any {
@@ -427,7 +461,7 @@
},
`is`(true)
)
- }.compilesWithoutError()
+ }
}
}
@@ -437,9 +471,7 @@
Triple(COMMON.RX2_COMPLETABLE, COMMON.RX2_ROOM, RxJava2TypeNames.COMPLETABLE),
Triple(COMMON.RX3_COMPLETABLE, COMMON.RX3_ROOM, RxJava3TypeNames.COMPLETABLE)
).forEach { (rxTypeSrc, _, rxTypeClassName) ->
- @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
- simpleRun(jfos = arrayOf(rxTypeSrc)) {
- invocation ->
+ runProcessorTest(sources = listOf(rxTypeSrc).toSources()) { invocation ->
val completable = invocation.processingEnv.requireTypeElement(rxTypeClassName)
assertThat(
RxCallableInsertMethodBinderProvider.getAll(invocation.context).any {
@@ -447,14 +479,13 @@
},
`is`(true)
)
- }.compilesWithoutError()
+ }
}
}
@Test
fun testFindInsertListenableFuture() {
- @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
- simpleRun(jfos = arrayOf(COMMON.LISTENABLE_FUTURE)) {
+ runProcessorTest(sources = listOf(COMMON.LISTENABLE_FUTURE).toSources()) {
invocation ->
val future = invocation.processingEnv
.requireTypeElement(GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE)
@@ -464,14 +495,12 @@
),
`is`(true)
)
- }.compilesWithoutError()
+ }
}
@Test
fun testFindDeleteOrUpdateSingle() {
- @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
- simpleRun(jfos = arrayOf(COMMON.RX2_SINGLE)) {
- invocation ->
+ runProcessorTest(sources = listOf(COMMON.RX2_SINGLE).toSources()) { invocation ->
val single = invocation.processingEnv.requireTypeElement(RxJava2TypeNames.SINGLE)
assertThat(single, notNullValue())
assertThat(
@@ -480,13 +509,12 @@
},
`is`(true)
)
- }.compilesWithoutError()
+ }
}
@Test
fun testFindDeleteOrUpdateMaybe() {
- @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
- simpleRun(jfos = arrayOf(COMMON.RX2_MAYBE)) {
+ runProcessorTest(sources = listOf(COMMON.RX2_MAYBE).toSources()) {
invocation ->
val maybe = invocation.processingEnv.requireTypeElement(RxJava2TypeNames.MAYBE)
assertThat(maybe, notNullValue())
@@ -496,13 +524,12 @@
},
`is`(true)
)
- }.compilesWithoutError()
+ }
}
@Test
fun testFindDeleteOrUpdateCompletable() {
- @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
- simpleRun(jfos = arrayOf(COMMON.RX2_COMPLETABLE)) {
+ runProcessorTest(sources = listOf(COMMON.RX2_COMPLETABLE).toSources()) {
invocation ->
val completable = invocation.processingEnv
.requireTypeElement(RxJava2TypeNames.COMPLETABLE)
@@ -513,14 +540,14 @@
},
`is`(true)
)
- }.compilesWithoutError()
+ }
}
@Test
fun testFindDeleteOrUpdateListenableFuture() {
- @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
- simpleRun(jfos = arrayOf(COMMON.LISTENABLE_FUTURE)) {
- invocation ->
+ runProcessorTest(
+ sources = listOf(COMMON.LISTENABLE_FUTURE).toSources()
+ ) { invocation ->
val future = invocation.processingEnv
.requireTypeElement(GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE)
assertThat(future, notNullValue())
@@ -529,14 +556,14 @@
.matches(future.asDeclaredType()),
`is`(true)
)
- }.compilesWithoutError()
+ }
}
@Test
fun testFindLiveData() {
- @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
- simpleRun(jfos = arrayOf(COMMON.COMPUTABLE_LIVE_DATA, COMMON.LIVE_DATA)) {
- invocation ->
+ runProcessorTest(
+ sources = listOf(COMMON.COMPUTABLE_LIVE_DATA, COMMON.LIVE_DATA).toSources()
+ ) { invocation ->
val liveData = invocation.processingEnv
.requireTypeElement(LifecyclesTypeNames.LIVE_DATA)
assertThat(liveData, notNullValue())
@@ -546,12 +573,12 @@
),
`is`(true)
)
- }.compilesWithoutError()
+ }
}
@Test
fun findPagingSourceIntKey() {
- simpleRun { invocation ->
+ runProcessorTest { invocation ->
val pagingSourceElement = invocation.processingEnv
.requireTypeElement(PagingSource::class)
val intType = invocation.processingEnv.requireType(Integer::class)
@@ -569,7 +596,7 @@
@Test
fun findPagingSourceStringKey() {
- simpleRun { invocation ->
+ runProcessorTest { invocation ->
val pagingSourceElement = invocation.processingEnv
.requireTypeElement(PagingSource::class)
val stringType = invocation.processingEnv.requireType(String::class)
@@ -582,12 +609,15 @@
.matches(pagingSourceIntIntType.asDeclaredType()),
`is`(true)
)
- }.failsToCompile().withErrorContaining(ProcessorErrors.PAGING_SPECIFY_PAGING_SOURCE_TYPE)
+ invocation.assertCompilationResult {
+ hasError(ProcessorErrors.PAGING_SPECIFY_PAGING_SOURCE_TYPE)
+ }
+ }
}
@Test
fun findDataSource() {
- simpleRun {
+ runProcessorTest {
invocation ->
val dataSource = invocation.processingEnv.requireTypeElement(DataSource::class)
assertThat(dataSource, notNullValue())
@@ -597,12 +627,15 @@
),
`is`(true)
)
- }.failsToCompile().withErrorContaining(ProcessorErrors.PAGING_SPECIFY_DATA_SOURCE_TYPE)
+ invocation.assertCompilationResult {
+ hasError(ProcessorErrors.PAGING_SPECIFY_DATA_SOURCE_TYPE)
+ }
+ }
}
@Test
fun findPositionalDataSource() {
- simpleRun {
+ runProcessorTest {
invocation ->
@Suppress("DEPRECATION")
val dataSource = invocation.processingEnv
@@ -614,13 +647,12 @@
),
`is`(true)
)
- }.compilesWithoutError()
+ }
}
@Test
fun findDataSourceFactory() {
- @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
- simpleRun(jfos = arrayOf(COMMON.DATA_SOURCE_FACTORY)) {
+ runProcessorTest(sources = listOf(COMMON.DATA_SOURCE_FACTORY).toSources()) {
invocation ->
val pagedListProvider = invocation.processingEnv
.requireTypeElement(PagingTypeNames.DATA_SOURCE_FACTORY)
@@ -631,14 +663,13 @@
),
`is`(true)
)
- }.compilesWithoutError()
+ }
}
- private fun createIntListToStringBinders(invocation: TestInvocation): List<TypeConverter> {
+ private fun createIntListToStringBinders(invocation: XTestInvocation): List<TypeConverter> {
val intType = invocation.processingEnv.requireType(Integer::class)
val listElement = invocation.processingEnv.requireTypeElement(java.util.List::class)
val listOfInts = invocation.processingEnv.getDeclaredType(listElement, intType)
-
val intListConverter = object : TypeConverter(
listOfInts,
invocation.context.COMMON_TYPES.STRING
@@ -676,53 +707,6 @@
return listOf(intListConverter, stringToIntListConverter)
}
- fun singleRun(handler: (TestInvocation) -> Unit): CompileTester {
- return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
- .that(
- listOf(
- JavaFileObjects.forSourceString(
- "foo.bar.DummyClass",
- """
- package foo.bar;
- import androidx.room.*;
- @Entity
- public class DummyClass {}
- """
- ),
- JavaFileObjects.forSourceString(
- "foo.bar.Point",
- """
- package foo.bar;
- import androidx.room.*;
- @Entity
- public class Point {
- public int x, y;
- public Point(int x, int y) {
- this.x = x;
- this.y = y;
- }
- public static Point fromBoolean(boolean val) {
- return val ? new Point(1, 1) : new Point(0, 0);
- }
- public static boolean toBoolean(Point point) {
- return point.x > 0;
- }
- }
- """
- )
- )
- )
- .processedWith(
- TestProcessor.builder()
- .forAnnotations(Entity::class)
- .nextRunHandler { invocation ->
- handler(invocation)
- true
- }
- .build()
- )
- }
-
fun pointTypeConverters(env: XProcessingEnv): List<TypeConverter> {
val tPoint = env.requireType("foo.bar.Point")
val tBoolean = env.requireType(TypeName.BOOLEAN)
@@ -759,8 +743,8 @@
}
fun dateTypeConverters(env: XProcessingEnv): List<TypeConverter> {
- val tDate = env.requireType("java.util.Date")
- val tLong = env.requireType("java.lang.Long")
+ val tDate = env.requireType("java.util.Date").makeNullable()
+ val tLong = env.requireType("java.lang.Long").makeNullable()
return listOf(
object : TypeConverter(tDate, tLong) {
override fun convert(
diff --git a/room/compiler/src/test/kotlin/androidx/room/testing/InProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/testing/InProcessorTest.kt
index 2d00be6..25c629a 100644
--- a/room/compiler/src/test/kotlin/androidx/room/testing/InProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/testing/InProcessorTest.kt
@@ -16,46 +16,37 @@
package androidx.room.testing
-import androidx.room.Query
-import com.google.common.truth.Truth
-import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourceSubjectFactory
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.MatcherAssert.assertThat
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.runProcessorTest
+import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-import java.util.concurrent.atomic.AtomicBoolean
@RunWith(JUnit4::class)
class InProcessorTest {
@Test
fun testInProcessorTestRuns() {
- val didRun = AtomicBoolean(false)
- Truth.assertAbout(JavaSourceSubjectFactory.javaSource())
- .that(
- JavaFileObjects.forSourceString(
- "foo.bar.MyClass",
- """
- package foo.bar;
- abstract public class MyClass {
- @androidx.room.Query("foo")
- abstract public void setFoo(String foo);
- }
- """
- )
- )
- .processedWith(
- TestProcessor.builder()
- .nextRunHandler { invocation ->
- didRun.set(true)
- assertThat(invocation.annotations.size, `is`(1))
- true
- }
- .forAnnotations(Query::class)
- .build()
- )
- .compilesWithoutError()
- assertThat(didRun.get(), `is`(true))
+ val source = Source.java(
+ qName = "foo.bar.MyClass",
+ code = """
+ package foo.bar;
+ abstract public class MyClass {
+ @androidx.room.Query("foo")
+ abstract public void setFoo(String foo);
+ }
+ """.trimIndent()
+ )
+ var runCount = 0
+ runProcessorTest(sources = listOf(source)) {
+ assertThat(
+ it.processingEnv.findTypeElement("foo.bar.MyClass")
+ ).isNotNull()
+ runCount++
+ }
+ // run 3 times: javac, kapt, ksp
+ assertThat(
+ runCount
+ ).isEqualTo(3)
}
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt b/room/compiler/src/test/kotlin/androidx/room/testing/XTestInvocationExt.kt
similarity index 71%
copy from compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
copy to room/compiler/src/test/kotlin/androidx/room/testing/XTestInvocationExt.kt
index f9cb2fe..62e386a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/testing/XTestInvocationExt.kt
@@ -14,7 +14,12 @@
* limitations under the License.
*/
-package androidx.compose.ui.focus
+package androidx.room.testing
-@RequiresOptIn("The Focus API is experimental and is likely to change in the future.")
-annotation class ExperimentalFocus
\ No newline at end of file
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.processor.Context
+
+val XTestInvocation.context
+ get() = getOrPutUserData(Context::class) {
+ Context(processingEnv)
+ }
\ No newline at end of file
diff --git a/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt b/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt
index b186bff..febe6e2 100644
--- a/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt
@@ -19,6 +19,7 @@
import androidx.room.compiler.processing.XElement
import androidx.room.compiler.processing.XFieldElement
import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.util.Source
import androidx.room.ext.GuavaUtilConcurrentTypeNames
import androidx.room.ext.KotlinTypeNames
import androidx.room.ext.LifecyclesTypeNames
@@ -314,4 +315,25 @@
return System.getProperty("java.class.path")!!.split(pathSeparator).map { File(it) }.toSet()
}
-fun String.toJFO(qName: String): JavaFileObject = JavaFileObjects.forSourceLines(qName, this)
\ No newline at end of file
+fun String.toJFO(qName: String): JavaFileObject = JavaFileObjects.forSourceLines(qName, this)
+
+/**
+ * Convenience method to convert JFO's to the Source objects in XProcessing so that we can
+ * convert room tests to the common API w/o major code refactor
+ */
+fun JavaFileObject.toSource(): Source {
+ val uri = this.toUri()
+ // parse name from uri
+ val contents = this.openReader(true).use {
+ it.readText()
+ }
+ val qName = uri.path.replace('/', '.')
+ val javaExt = ".java"
+ check(qName.endsWith(javaExt)) {
+ "expected a java source file, $qName does not seem like one"
+ }
+
+ return Source.java(qName.dropLast(javaExt.length), contents)
+}
+
+fun Collection<JavaFileObject>.toSources() = map { it.toSource() }
diff --git a/room/compiler/src/test/kotlin/androidx/room/util/SimpleJavaVersionTest.kt b/room/compiler/src/test/kotlin/androidx/room/util/SimpleJavaVersionTest.kt
index 830aeba..e930fc9 100644
--- a/room/compiler/src/test/kotlin/androidx/room/util/SimpleJavaVersionTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/util/SimpleJavaVersionTest.kt
@@ -16,6 +16,7 @@
package androidx.room.util
+import com.google.common.truth.Truth.assertThat
import org.junit.Assert.fail
import org.junit.Test
@@ -23,37 +24,39 @@
@Test
fun testTryParse() {
- assert(SimpleJavaVersion.tryParse("11.0.1+13-LTS") == SimpleJavaVersion(11, 0, null))
- assert(
- SimpleJavaVersion.tryParse("11.0.6+10-post-Ubuntu-1ubuntu118.04.1")
- == SimpleJavaVersion(11, 0, null)
- )
- assert(
- SimpleJavaVersion.tryParse("1.8.0_202-release-1483-b39-5396753")
- == SimpleJavaVersion(8, 0, 202)
- )
- assert(
- SimpleJavaVersion.tryParse("1.8.0_181-google-v7-238857965-238857965")
- == SimpleJavaVersion(8, 0, 181)
- )
- assert(SimpleJavaVersion.tryParse("a.b.c") == null)
+ assertThat(SimpleJavaVersion.tryParse("1.8.0_181-google-v7-238857965-238857965"))
+ .isEqualTo(SimpleJavaVersion(8, 0, 181))
+ assertThat(SimpleJavaVersion.tryParse("1.8.0_202-release-1483-b39-5396753"))
+ .isEqualTo(SimpleJavaVersion(8, 0, 202))
+ assertThat(SimpleJavaVersion.tryParse("11.0.1+13-LTS"))
+ .isEqualTo(SimpleJavaVersion(11, 0, null))
+ assertThat(SimpleJavaVersion.tryParse("11.0.6+10-post-Ubuntu-1ubuntu118.04.1"))
+ .isEqualTo(SimpleJavaVersion(11, 0, null))
+ assertThat(SimpleJavaVersion.tryParse("11.0.8+10-b944.6842174"))
+ .isEqualTo(SimpleJavaVersion(11, 0, null))
+ assertThat(SimpleJavaVersion.tryParse("14.1-ea"))
+ .isEqualTo(SimpleJavaVersion(14, 1, null))
+ assertThat(SimpleJavaVersion.tryParse("15+13"))
+ .isEqualTo(SimpleJavaVersion(15, 0, null))
+ assertThat(SimpleJavaVersion.tryParse("a.b.c")).isNull()
}
@Test
fun testParse() {
- assert(SimpleJavaVersion.parse("11.0.1+13-LTS") == SimpleJavaVersion(11, 0, null))
- assert(
- SimpleJavaVersion.parse("11.0.6+10-post-Ubuntu-1ubuntu118.04.1")
- == SimpleJavaVersion(11, 0, null)
- )
- assert(
- SimpleJavaVersion.parse("1.8.0_202-release-1483-b39-5396753")
- == SimpleJavaVersion(8, 0, 202)
- )
- assert(
- SimpleJavaVersion.parse("1.8.0_181-google-v7-238857965-238857965")
- == SimpleJavaVersion(8, 0, 181)
- )
+ assertThat(SimpleJavaVersion.parse("1.8.0_181-google-v7-238857965-238857965"))
+ .isEqualTo(SimpleJavaVersion(8, 0, 181))
+ assertThat(SimpleJavaVersion.parse("1.8.0_202-release-1483-b39-5396753"))
+ .isEqualTo(SimpleJavaVersion(8, 0, 202))
+ assertThat(SimpleJavaVersion.parse("11.0.1+13-LTS"))
+ .isEqualTo(SimpleJavaVersion(11, 0, null))
+ assertThat(SimpleJavaVersion.parse("11.0.6+10-post-Ubuntu-1ubuntu118.04.1"))
+ .isEqualTo(SimpleJavaVersion(11, 0, null))
+ assertThat(SimpleJavaVersion.parse("11.0.8+10-b944.6842174"))
+ .isEqualTo(SimpleJavaVersion(11, 0, null))
+ assertThat(SimpleJavaVersion.parse("14.1-ea"))
+ .isEqualTo(SimpleJavaVersion(14, 1, null))
+ assertThat(SimpleJavaVersion.parse("15+13"))
+ .isEqualTo(SimpleJavaVersion(15, 0, null))
try {
SimpleJavaVersion.parse("a.b.c")
fail("Expected IllegalArgumentException")
@@ -64,19 +67,19 @@
@Test
fun testComparison() {
- assert(SimpleJavaVersion(11, 1) > SimpleJavaVersion(8, 2))
- assert(SimpleJavaVersion(8, 2) < SimpleJavaVersion(11, 1))
- assert(SimpleJavaVersion(8, 1) == SimpleJavaVersion(8, 1))
+ assertThat(SimpleJavaVersion(11, 1)).isGreaterThan(SimpleJavaVersion(8, 2))
+ assertThat(SimpleJavaVersion(8, 2)).isLessThan(SimpleJavaVersion(11, 1))
+ assertThat(SimpleJavaVersion(8, 1)).isEqualTo(SimpleJavaVersion(8, 1))
- assert(SimpleJavaVersion(8, 2, 1) > SimpleJavaVersion(8, 1, 2))
- assert(SimpleJavaVersion(8, 1, 2) < SimpleJavaVersion(8, 2, 1))
- assert(SimpleJavaVersion(8, 1, null) == SimpleJavaVersion(8, 1, null))
+ assertThat(SimpleJavaVersion(8, 2, 1)).isGreaterThan(SimpleJavaVersion(8, 1, 2))
+ assertThat(SimpleJavaVersion(8, 1, 2)).isLessThan(SimpleJavaVersion(8, 2, 1))
+ assertThat(SimpleJavaVersion(8, 1, null)).isEqualTo(SimpleJavaVersion(8, 1, null))
- assert(SimpleJavaVersion(8, 1, 2) > SimpleJavaVersion(8, 1, 1))
- assert(SimpleJavaVersion(8, 1, 1) < SimpleJavaVersion(8, 1, 2))
- assert(SimpleJavaVersion(8, 1, 1) == SimpleJavaVersion(8, 1, 1))
+ assertThat(SimpleJavaVersion(8, 1, 2)).isGreaterThan(SimpleJavaVersion(8, 1, 1))
+ assertThat(SimpleJavaVersion(8, 1, 1)).isLessThan(SimpleJavaVersion(8, 1, 2))
+ assertThat(SimpleJavaVersion(8, 1, 1)).isEqualTo(SimpleJavaVersion(8, 1, 1))
- assert(SimpleJavaVersion(8, 1, 0) > SimpleJavaVersion(8, 1, null))
- assert(SimpleJavaVersion(8, 1, null) < SimpleJavaVersion(8, 1, 0))
+ assertThat(SimpleJavaVersion(8, 1, 0)).isGreaterThan(SimpleJavaVersion(8, 1, null))
+ assertThat(SimpleJavaVersion(8, 1, null)).isLessThan(SimpleJavaVersion(8, 1, 0))
}
}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt
new file mode 100644
index 0000000..8ad0e02
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt
@@ -0,0 +1,243 @@
+/*
+ * 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.integration.kotlintestapp.test
+
+import androidx.arch.core.executor.testing.CountingTaskExecutorRule
+import androidx.room.Dao
+import androidx.room.Database
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.Update
+import androidx.sqlite.db.SimpleSQLiteQuery
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.CopyOnWriteArrayList
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class QueryInterceptorTest {
+ @Rule
+ @JvmField
+ val countingTaskExecutorRule = CountingTaskExecutorRule()
+ lateinit var mDatabase: QueryInterceptorTestDatabase
+ var queryAndArgs = CopyOnWriteArrayList<Pair<String, ArrayList<Any>>>()
+
+ @Entity(tableName = "queryInterceptorTestDatabase")
+ data class QueryInterceptorEntity(@PrimaryKey val id: String, val description: String)
+
+ @Dao
+ interface QueryInterceptorDao {
+ @Query("DELETE FROM queryInterceptorTestDatabase WHERE id=:id")
+ fun delete(id: String)
+
+ @Insert
+ fun insert(item: QueryInterceptorEntity)
+
+ @Update
+ fun update(vararg item: QueryInterceptorEntity)
+ }
+
+ @Database(
+ version = 1,
+ entities = [
+ QueryInterceptorEntity::class
+ ],
+ exportSchema = false
+ )
+ abstract class QueryInterceptorTestDatabase : RoomDatabase() {
+ abstract fun queryInterceptorDao(): QueryInterceptorDao
+ }
+
+ @Before
+ fun setUp() {
+ mDatabase = Room.inMemoryDatabaseBuilder(
+ ApplicationProvider.getApplicationContext(),
+ QueryInterceptorTestDatabase::class.java
+ ).setQueryCallback(
+ RoomDatabase.QueryCallback { sqlQuery, bindArgs ->
+ val argTrace = ArrayList<Any>()
+ argTrace.addAll(bindArgs)
+ queryAndArgs.add(Pair(sqlQuery, argTrace))
+ },
+ MoreExecutors.directExecutor()
+ ).build()
+ }
+
+ @After
+ fun tearDown() {
+ mDatabase.close()
+ }
+
+ @Test
+ fun testInsert() {
+ mDatabase.queryInterceptorDao().insert(
+ QueryInterceptorEntity("Insert", "Inserted a placeholder query")
+ )
+
+ assertQueryLogged(
+ "INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`,`description`) " +
+ "VALUES (?,?)",
+ listOf("Insert", "Inserted a placeholder query")
+ )
+ assertTransactionQueries()
+ }
+
+ @Test
+ fun testDelete() {
+ mDatabase.queryInterceptorDao().delete("Insert")
+ assertQueryLogged(
+ "DELETE FROM queryInterceptorTestDatabase WHERE id=?",
+ listOf("Insert")
+ )
+ assertTransactionQueries()
+ }
+
+ @Test
+ fun testUpdate() {
+ mDatabase.queryInterceptorDao().insert(
+ QueryInterceptorEntity("Insert", "Inserted a placeholder query")
+ )
+ mDatabase.queryInterceptorDao().update(
+ QueryInterceptorEntity("Insert", "Updated the placeholder query")
+ )
+
+ assertQueryLogged(
+ "UPDATE OR ABORT `queryInterceptorTestDatabase` SET `id` " +
+ "= ?,`description` = ? " +
+ "WHERE `id` = ?",
+ listOf("Insert", "Updated the placeholder query", "Insert")
+ )
+ assertTransactionQueries()
+ }
+
+ @Test
+ fun testCompileStatement() {
+ assertEquals(queryAndArgs.size, 0)
+ mDatabase.queryInterceptorDao().insert(
+ QueryInterceptorEntity("Insert", "Inserted a placeholder query")
+ )
+ mDatabase.openHelper.writableDatabase.compileStatement(
+ "DELETE FROM queryInterceptorTestDatabase WHERE id=?"
+ ).execute()
+ assertQueryLogged("DELETE FROM queryInterceptorTestDatabase WHERE id=?", emptyList())
+ }
+
+ @Test
+ fun testLoggingSupportSQLiteQuery() {
+ mDatabase.openHelper.writableDatabase.query(
+ SimpleSQLiteQuery(
+ "INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`,`description`) " +
+ "VALUES (?,?)",
+ arrayOf<Any>("3", "Description")
+ )
+ )
+ assertQueryLogged(
+ "INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`,`description`) " +
+ "VALUES (?,?)",
+ listOf("3", "Description")
+ )
+ }
+
+ @Test
+ fun testNullBindArgument() {
+ mDatabase.openHelper.writableDatabase.query(
+ SimpleSQLiteQuery(
+ "INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`,`description`) " +
+ "VALUES (?,?)",
+ arrayOf("ID", null)
+ )
+ )
+ assertQueryLogged(
+ "INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`," +
+ "`description`) VALUES (?,?)",
+ listOf("ID", null)
+ )
+ }
+
+ @Test
+ fun testCallbackCalledOnceAfterCloseAndReOpen() {
+ val dbBuilder = Room.inMemoryDatabaseBuilder(
+ ApplicationProvider.getApplicationContext(),
+ QueryInterceptorTestDatabase::class.java
+ ).setQueryCallback(
+ RoomDatabase.QueryCallback { sqlQuery, bindArgs ->
+ val argTrace = ArrayList<Any>()
+ argTrace.addAll(bindArgs)
+ queryAndArgs.add(Pair(sqlQuery, argTrace))
+ },
+ MoreExecutors.directExecutor()
+ )
+
+ dbBuilder.build().close()
+
+ mDatabase = dbBuilder.build()
+
+ mDatabase.queryInterceptorDao().insert(
+ QueryInterceptorEntity("Insert", "Inserted a placeholder query")
+ )
+
+ assertQueryLogged(
+ "INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`,`description`) " +
+ "VALUES (?,?)",
+ listOf("Insert", "Inserted a placeholder query")
+ )
+ assertTransactionQueries()
+ }
+
+ private fun assertQueryLogged(
+ query: String,
+ expectedArgs: List<String?>
+ ) {
+ val filteredQueries = queryAndArgs.filter {
+ it.first == query
+ }
+ assertThat(filteredQueries).hasSize(1)
+ assertThat(expectedArgs).containsExactlyElementsIn(filteredQueries[0].second)
+ }
+
+ private fun assertTransactionQueries() {
+ assertNotNull(
+ queryAndArgs.any {
+ it.equals("BEGIN TRANSACTION")
+ }
+ )
+ assertNotNull(
+ queryAndArgs.any {
+ it.equals("TRANSACTION SUCCESSFUL")
+ }
+ )
+ assertNotNull(
+ queryAndArgs.any {
+ it.equals("END TRANSACTION")
+ }
+ )
+ }
+}
diff --git a/room/runtime/api/current.txt b/room/runtime/api/current.txt
index e77a927..061b0ad 100644
--- a/room/runtime/api/current.txt
+++ b/room/runtime/api/current.txt
@@ -87,6 +87,7 @@
method public androidx.room.RoomDatabase.Builder<T!> fallbackToDestructiveMigrationOnDowngrade();
method public androidx.room.RoomDatabase.Builder<T!> openHelperFactory(androidx.sqlite.db.SupportSQLiteOpenHelper.Factory?);
method public androidx.room.RoomDatabase.Builder<T!> setJournalMode(androidx.room.RoomDatabase.JournalMode);
+ method public androidx.room.RoomDatabase.Builder<T!> setQueryCallback(androidx.room.RoomDatabase.QueryCallback, java.util.concurrent.Executor);
method public androidx.room.RoomDatabase.Builder<T!> setQueryExecutor(java.util.concurrent.Executor);
method public androidx.room.RoomDatabase.Builder<T!> setTransactionExecutor(java.util.concurrent.Executor);
}
@@ -115,6 +116,10 @@
method public void onOpenPrepackagedDatabase(androidx.sqlite.db.SupportSQLiteDatabase);
}
+ public static interface RoomDatabase.QueryCallback {
+ method public void onQuery(String, java.util.List<java.lang.Object!>);
+ }
+
}
package androidx.room.migration {
diff --git a/room/runtime/api/public_plus_experimental_current.txt b/room/runtime/api/public_plus_experimental_current.txt
index a35e99f..e1cefb2 100644
--- a/room/runtime/api/public_plus_experimental_current.txt
+++ b/room/runtime/api/public_plus_experimental_current.txt
@@ -88,6 +88,7 @@
method public androidx.room.RoomDatabase.Builder<T!> fallbackToDestructiveMigrationOnDowngrade();
method public androidx.room.RoomDatabase.Builder<T!> openHelperFactory(androidx.sqlite.db.SupportSQLiteOpenHelper.Factory?);
method public androidx.room.RoomDatabase.Builder<T!> setJournalMode(androidx.room.RoomDatabase.JournalMode);
+ method public androidx.room.RoomDatabase.Builder<T!> setQueryCallback(androidx.room.RoomDatabase.QueryCallback, java.util.concurrent.Executor);
method public androidx.room.RoomDatabase.Builder<T!> setQueryExecutor(java.util.concurrent.Executor);
method public androidx.room.RoomDatabase.Builder<T!> setTransactionExecutor(java.util.concurrent.Executor);
}
@@ -116,6 +117,10 @@
method public void onOpenPrepackagedDatabase(androidx.sqlite.db.SupportSQLiteDatabase);
}
+ public static interface RoomDatabase.QueryCallback {
+ method public void onQuery(String, java.util.List<java.lang.Object!>);
+ }
+
}
package androidx.room.migration {
diff --git a/room/runtime/api/restricted_current.txt b/room/runtime/api/restricted_current.txt
index b875d71..4b2d12a 100644
--- a/room/runtime/api/restricted_current.txt
+++ b/room/runtime/api/restricted_current.txt
@@ -130,6 +130,7 @@
method public androidx.room.RoomDatabase.Builder<T!> fallbackToDestructiveMigrationOnDowngrade();
method public androidx.room.RoomDatabase.Builder<T!> openHelperFactory(androidx.sqlite.db.SupportSQLiteOpenHelper.Factory?);
method public androidx.room.RoomDatabase.Builder<T!> setJournalMode(androidx.room.RoomDatabase.JournalMode);
+ method public androidx.room.RoomDatabase.Builder<T!> setQueryCallback(androidx.room.RoomDatabase.QueryCallback, java.util.concurrent.Executor);
method public androidx.room.RoomDatabase.Builder<T!> setQueryExecutor(java.util.concurrent.Executor);
method public androidx.room.RoomDatabase.Builder<T!> setTransactionExecutor(java.util.concurrent.Executor);
}
@@ -158,6 +159,10 @@
method public void onOpenPrepackagedDatabase(androidx.sqlite.db.SupportSQLiteDatabase);
}
+ public static interface RoomDatabase.QueryCallback {
+ method public void onQuery(String, java.util.List<java.lang.Object!>);
+ }
+
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class RoomOpenHelper extends androidx.sqlite.db.SupportSQLiteOpenHelper.Callback {
ctor public RoomOpenHelper(androidx.room.DatabaseConfiguration, androidx.room.RoomOpenHelper.Delegate, String, String);
ctor public RoomOpenHelper(androidx.room.DatabaseConfiguration, androidx.room.RoomOpenHelper.Delegate, String);
diff --git a/room/runtime/src/main/java/androidx/room/QueryInterceptorDatabase.java b/room/runtime/src/main/java/androidx/room/QueryInterceptorDatabase.java
new file mode 100644
index 0000000..c5ef6bc
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/QueryInterceptorDatabase.java
@@ -0,0 +1,302 @@
+/*
+ * 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;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteTransactionListener;
+import android.os.Build;
+import android.os.CancellationSignal;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteQuery;
+import androidx.sqlite.db.SupportSQLiteStatement;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.Executor;
+
+
+/**
+ * Implements {@link SupportSQLiteDatabase} for SQLite queries.
+ */
+final class QueryInterceptorDatabase implements SupportSQLiteDatabase {
+
+ private final SupportSQLiteDatabase mDelegate;
+ private final RoomDatabase.QueryCallback mQueryCallback;
+ private final Executor mQueryCallbackExecutor;
+
+ QueryInterceptorDatabase(@NonNull SupportSQLiteDatabase supportSQLiteDatabase,
+ @NonNull RoomDatabase.QueryCallback queryCallback, @NonNull Executor
+ queryCallbackExecutor) {
+ mDelegate = supportSQLiteDatabase;
+ mQueryCallback = queryCallback;
+ mQueryCallbackExecutor = queryCallbackExecutor;
+ }
+
+ @NonNull
+ @Override
+ public SupportSQLiteStatement compileStatement(@NonNull String sql) {
+ return new QueryInterceptorStatement(mDelegate.compileStatement(sql),
+ mQueryCallback, sql, mQueryCallbackExecutor);
+ }
+
+ @Override
+ public void beginTransaction() {
+ mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION",
+ Collections.emptyList()));
+ mDelegate.beginTransaction();
+ }
+
+ @Override
+ public void beginTransactionNonExclusive() {
+ mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN DEFERRED TRANSACTION",
+ Collections.emptyList()));
+ mDelegate.beginTransactionNonExclusive();
+ }
+
+ @Override
+ public void beginTransactionWithListener(@NonNull SQLiteTransactionListener
+ transactionListener) {
+ mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION",
+ Collections.emptyList()));
+ mDelegate.beginTransactionWithListener(transactionListener);
+ }
+
+ @Override
+ public void beginTransactionWithListenerNonExclusive(
+ @NonNull SQLiteTransactionListener transactionListener) {
+ mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN DEFERRED TRANSACTION",
+ Collections.emptyList()));
+ mDelegate.beginTransactionWithListenerNonExclusive(transactionListener);
+ }
+
+ @Override
+ public void endTransaction() {
+ mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("END TRANSACTION",
+ Collections.emptyList()));
+ mDelegate.endTransaction();
+ }
+
+ @Override
+ public void setTransactionSuccessful() {
+ mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("TRANSACTION SUCCESSFUL",
+ Collections.emptyList()));
+ mDelegate.setTransactionSuccessful();
+ }
+
+ @Override
+ public boolean inTransaction() {
+ return mDelegate.inTransaction();
+ }
+
+ @Override
+ public boolean isDbLockedByCurrentThread() {
+ return mDelegate.isDbLockedByCurrentThread();
+ }
+
+ @Override
+ public boolean yieldIfContendedSafely() {
+ return mDelegate.yieldIfContendedSafely();
+ }
+
+ @Override
+ public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) {
+ return mDelegate.yieldIfContendedSafely(sleepAfterYieldDelay);
+ }
+
+ @Override
+ public int getVersion() {
+ return mDelegate.getVersion();
+ }
+
+ @Override
+ public void setVersion(int version) {
+ mDelegate.setVersion(version);
+ }
+
+ @Override
+ public long getMaximumSize() {
+ return mDelegate.getMaximumSize();
+ }
+
+ @Override
+ public long setMaximumSize(long numBytes) {
+ return mDelegate.setMaximumSize(numBytes);
+ }
+
+ @Override
+ public long getPageSize() {
+ return mDelegate.getPageSize();
+ }
+
+ @Override
+ public void setPageSize(long numBytes) {
+ mDelegate.setPageSize(numBytes);
+ }
+
+ @NonNull
+ @Override
+ public Cursor query(@NonNull String query) {
+ mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query,
+ Collections.emptyList()));
+ return mDelegate.query(query);
+ }
+
+ @NonNull
+ @Override
+ public Cursor query(@NonNull String query, @NonNull Object[] bindArgs) {
+ List<Object> inputArguments = new ArrayList<>();
+ inputArguments.addAll(Arrays.asList(bindArgs));
+ mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query,
+ inputArguments));
+ return mDelegate.query(query, bindArgs);
+ }
+
+ @NonNull
+ @Override
+ public Cursor query(@NonNull SupportSQLiteQuery query) {
+ QueryInterceptorProgram queryInterceptorProgram = new QueryInterceptorProgram();
+ query.bindTo(queryInterceptorProgram);
+ mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query.getSql(),
+ queryInterceptorProgram.getBindArgs()));
+ return mDelegate.query(query);
+ }
+
+ @NonNull
+ @Override
+ public Cursor query(@NonNull SupportSQLiteQuery query,
+ @NonNull CancellationSignal cancellationSignal) {
+ QueryInterceptorProgram queryInterceptorProgram = new QueryInterceptorProgram();
+ query.bindTo(queryInterceptorProgram);
+ mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query.getSql(),
+ queryInterceptorProgram.getBindArgs()));
+ return mDelegate.query(query);
+ }
+
+ @Override
+ public long insert(@NonNull String table, int conflictAlgorithm, @NonNull ContentValues values)
+ throws SQLException {
+ return mDelegate.insert(table, conflictAlgorithm, values);
+ }
+
+ @Override
+ public int delete(@NonNull String table, @NonNull String whereClause,
+ @NonNull Object[] whereArgs) {
+ return mDelegate.delete(table, whereClause, whereArgs);
+ }
+
+ @Override
+ public int update(@NonNull String table, int conflictAlgorithm, @NonNull ContentValues values,
+ @NonNull String whereClause,
+ @NonNull Object[] whereArgs) {
+ return mDelegate.update(table, conflictAlgorithm, values, whereClause,
+ whereArgs);
+ }
+
+ @Override
+ public void execSQL(@NonNull String sql) throws SQLException {
+ mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(sql, new ArrayList<>(0)));
+ mDelegate.execSQL(sql);
+ }
+
+ @Override
+ public void execSQL(@NonNull String sql, @NonNull Object[] bindArgs) throws SQLException {
+ List<Object> inputArguments = new ArrayList<>();
+ inputArguments.addAll(Arrays.asList(bindArgs));
+ mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(sql, inputArguments));
+ mDelegate.execSQL(sql, inputArguments.toArray());
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return mDelegate.isReadOnly();
+ }
+
+ @Override
+ public boolean isOpen() {
+ return mDelegate.isOpen();
+ }
+
+ @Override
+ public boolean needUpgrade(int newVersion) {
+ return mDelegate.needUpgrade(newVersion);
+ }
+
+ @NonNull
+ @Override
+ public String getPath() {
+ return mDelegate.getPath();
+ }
+
+ @Override
+ public void setLocale(@NonNull Locale locale) {
+ mDelegate.setLocale(locale);
+ }
+
+ @Override
+ public void setMaxSqlCacheSize(int cacheSize) {
+ mDelegate.setMaxSqlCacheSize(cacheSize);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+ @Override
+ public void setForeignKeyConstraintsEnabled(boolean enable) {
+ mDelegate.setForeignKeyConstraintsEnabled(enable);
+ }
+
+ @Override
+ public boolean enableWriteAheadLogging() {
+ return mDelegate.enableWriteAheadLogging();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+ @Override
+ public void disableWriteAheadLogging() {
+ mDelegate.disableWriteAheadLogging();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+ @Override
+ public boolean isWriteAheadLoggingEnabled() {
+ return mDelegate.isWriteAheadLoggingEnabled();
+ }
+
+ @NonNull
+ @Override
+ public List<Pair<String, String>> getAttachedDbs() {
+ return mDelegate.getAttachedDbs();
+ }
+
+ @Override
+ public boolean isDatabaseIntegrityOk() {
+ return mDelegate.isDatabaseIntegrityOk();
+ }
+
+ @Override
+ public void close() throws IOException {
+ mDelegate.close();
+ }
+}
diff --git a/room/runtime/src/main/java/androidx/room/QueryInterceptorOpenHelper.java b/room/runtime/src/main/java/androidx/room/QueryInterceptorOpenHelper.java
new file mode 100644
index 0000000..c2ca486
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/QueryInterceptorOpenHelper.java
@@ -0,0 +1,71 @@
+/*
+ * 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;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+
+import java.util.concurrent.Executor;
+
+final class QueryInterceptorOpenHelper implements SupportSQLiteOpenHelper {
+
+ private final SupportSQLiteOpenHelper mDelegate;
+ private final RoomDatabase.QueryCallback mQueryCallback;
+ private final Executor mQueryCallbackExecutor;
+
+ QueryInterceptorOpenHelper(@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper,
+ @NonNull RoomDatabase.QueryCallback queryCallback, @NonNull Executor
+ queryCallbackExecutor) {
+ mDelegate = supportSQLiteOpenHelper;
+ mQueryCallback = queryCallback;
+ mQueryCallbackExecutor = queryCallbackExecutor;
+ }
+
+ @Nullable
+ @Override
+ public String getDatabaseName() {
+ return mDelegate.getDatabaseName();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+ @Override
+ public void setWriteAheadLoggingEnabled(boolean enabled) {
+ mDelegate.setWriteAheadLoggingEnabled(enabled);
+ }
+
+ @Override
+ public SupportSQLiteDatabase getWritableDatabase() {
+ return new QueryInterceptorDatabase(mDelegate.getWritableDatabase(), mQueryCallback,
+ mQueryCallbackExecutor);
+ }
+
+ @Override
+ public SupportSQLiteDatabase getReadableDatabase() {
+ return new QueryInterceptorDatabase(mDelegate.getReadableDatabase(), mQueryCallback,
+ mQueryCallbackExecutor);
+ }
+
+ @Override
+ public void close() {
+ mDelegate.close();
+ }
+}
diff --git a/room/runtime/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.java b/room/runtime/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.java
new file mode 100644
index 0000000..5d94cd1
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.java
@@ -0,0 +1,50 @@
+/*
+ * 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;
+
+import androidx.annotation.NonNull;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Implements {@link SupportSQLiteOpenHelper.Factory} to wrap QueryInterceptorOpenHelper.
+ */
+@SuppressWarnings("AcronymName")
+final class QueryInterceptorOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
+
+ private final SupportSQLiteOpenHelper.Factory mDelegate;
+ private final RoomDatabase.QueryCallback mQueryCallback;
+ private final Executor mQueryCallbackExecutor;
+
+ @SuppressWarnings("LambdaLast")
+ QueryInterceptorOpenHelperFactory(@NonNull SupportSQLiteOpenHelper.Factory factory,
+ @NonNull RoomDatabase.QueryCallback queryCallback,
+ @NonNull Executor queryCallbackExecutor) {
+ mDelegate = factory;
+ mQueryCallback = queryCallback;
+ mQueryCallbackExecutor = queryCallbackExecutor;
+ }
+
+ @NonNull
+ @Override
+ public SupportSQLiteOpenHelper create(
+ @NonNull SupportSQLiteOpenHelper.Configuration configuration) {
+ return new QueryInterceptorOpenHelper(mDelegate.create(configuration), mQueryCallback,
+ mQueryCallbackExecutor);
+ }
+}
diff --git a/room/runtime/src/main/java/androidx/room/QueryInterceptorProgram.java b/room/runtime/src/main/java/androidx/room/QueryInterceptorProgram.java
new file mode 100644
index 0000000..2b9c554
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/QueryInterceptorProgram.java
@@ -0,0 +1,82 @@
+/*
+ * 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;
+
+import androidx.sqlite.db.SupportSQLiteProgram;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A program implementing an {@link SupportSQLiteProgram} API to record bind arguments.
+ */
+final class QueryInterceptorProgram implements SupportSQLiteProgram {
+ private List<Object> mBindArgsCache = new ArrayList<>();
+
+ @Override
+ public void bindNull(int index) {
+ saveArgsToCache(index, null);
+ }
+
+ @Override
+ public void bindLong(int index, long value) {
+ saveArgsToCache(index, value);
+ }
+
+ @Override
+ public void bindDouble(int index, double value) {
+ saveArgsToCache(index, value);
+ }
+
+ @Override
+ public void bindString(int index, String value) {
+ saveArgsToCache(index, value);
+ }
+
+ @Override
+ public void bindBlob(int index, byte[] value) {
+ saveArgsToCache(index, value);
+ }
+
+ @Override
+ public void clearBindings() {
+ mBindArgsCache.clear();
+ }
+
+ @Override
+ public void close() { }
+
+ private void saveArgsToCache(int bindIndex, Object value) {
+ // The index into bind methods are 1...n
+ int index = bindIndex - 1;
+ if (index >= mBindArgsCache.size()) {
+ for (int i = mBindArgsCache.size(); i <= index; i++) {
+ mBindArgsCache.add(null);
+ }
+ }
+ mBindArgsCache.set(index, value);
+ }
+
+ /**
+ * Returns the list of arguments associated with the query.
+ *
+ * @return argument list.
+ */
+ List<Object> getBindArgs() {
+ return mBindArgsCache;
+ }
+}
diff --git a/room/runtime/src/main/java/androidx/room/QueryInterceptorStatement.java b/room/runtime/src/main/java/androidx/room/QueryInterceptorStatement.java
new file mode 100644
index 0000000..8825252
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/QueryInterceptorStatement.java
@@ -0,0 +1,128 @@
+/*
+ * 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;
+
+import androidx.annotation.NonNull;
+import androidx.sqlite.db.SupportSQLiteStatement;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Implements an instance of {@link SupportSQLiteStatement} for SQLite queries.
+ */
+final class QueryInterceptorStatement implements SupportSQLiteStatement {
+
+ private final SupportSQLiteStatement mDelegate;
+ private final RoomDatabase.QueryCallback mQueryCallback;
+ private final String mSqlStatement;
+ private final List<Object> mBindArgsCache = new ArrayList<>();
+ private final Executor mQueryCallbackExecutor;
+
+ QueryInterceptorStatement(@NonNull SupportSQLiteStatement compileStatement,
+ @NonNull RoomDatabase.QueryCallback queryCallback, String sqlStatement,
+ @NonNull Executor queryCallbackExecutor) {
+ mDelegate = compileStatement;
+ mQueryCallback = queryCallback;
+ mSqlStatement = sqlStatement;
+ mQueryCallbackExecutor = queryCallbackExecutor;
+ }
+
+ @Override
+ public void execute() {
+ mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
+ mDelegate.execute();
+ }
+
+ @Override
+ public int executeUpdateDelete() {
+ mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
+ return mDelegate.executeUpdateDelete();
+ }
+
+ @Override
+ public long executeInsert() {
+ mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
+ return mDelegate.executeInsert();
+ }
+
+ @Override
+ public long simpleQueryForLong() {
+ mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
+ return mDelegate.simpleQueryForLong();
+ }
+
+ @Override
+ public String simpleQueryForString() {
+ mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
+ return mDelegate.simpleQueryForString();
+ }
+
+ @Override
+ public void bindNull(int index) {
+ saveArgsToCache(index, mBindArgsCache.toArray());
+ mDelegate.bindNull(index);
+ }
+
+ @Override
+ public void bindLong(int index, long value) {
+ saveArgsToCache(index, value);
+ mDelegate.bindLong(index, value);
+ }
+
+ @Override
+ public void bindDouble(int index, double value) {
+ saveArgsToCache(index, value);
+ mDelegate.bindDouble(index, value);
+ }
+
+ @Override
+ public void bindString(int index, String value) {
+ saveArgsToCache(index, value);
+ mDelegate.bindString(index, value);
+ }
+
+ @Override
+ public void bindBlob(int index, byte[] value) {
+ saveArgsToCache(index, value);
+ mDelegate.bindBlob(index, value);
+ }
+
+ @Override
+ public void clearBindings() {
+ mBindArgsCache.clear();
+ mDelegate.clearBindings();
+ }
+
+ @Override
+ public void close() throws IOException {
+ mDelegate.close();
+ }
+
+ private void saveArgsToCache(int bindIndex, Object value) {
+ int index = bindIndex - 1;
+ if (index >= mBindArgsCache.size()) {
+ // Add null entries to the list until we have the desired # of indices
+ for (int i = mBindArgsCache.size(); i <= index; i++) {
+ mBindArgsCache.add(null);
+ }
+ }
+ mBindArgsCache.set(index, value);
+ }
+}
diff --git a/room/runtime/src/main/java/androidx/room/RoomDatabase.java b/room/runtime/src/main/java/androidx/room/RoomDatabase.java
index 7fbf181..4561876 100644
--- a/room/runtime/src/main/java/androidx/room/RoomDatabase.java
+++ b/room/runtime/src/main/java/androidx/room/RoomDatabase.java
@@ -620,6 +620,8 @@
private final Context mContext;
private ArrayList<Callback> mCallbacks;
private PrepackagedDatabaseCallback mPrepackagedDatabaseCallback;
+ private QueryCallback mQueryCallback;
+ private Executor mQueryCallbackExecutor;
private List<Object> mTypeConverters;
/** The Executor used to run database queries. This should be background-threaded. */
@@ -1092,6 +1094,27 @@
}
/**
+ * Sets a {@link QueryCallback} to be invoked when queries are executed.
+ * <p>
+ * The callback is invoked whenever a query is executed, note that adding this callback
+ * has a small cost and should be avoided in production builds unless needed.
+ * <p>
+ * A use case for providing a callback is to allow logging executed queries. When the
+ * callback implementation logs then it is recommended to use an immediate executor.
+ *
+ * @param queryCallback The query callback.
+ * @param executor The executor on which the query callback will be invoked.
+ */
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ @NonNull
+ public Builder<T> setQueryCallback(@NonNull QueryCallback queryCallback,
+ @NonNull Executor executor) {
+ mQueryCallback = queryCallback;
+ mQueryCallbackExecutor = executor;
+ return this;
+ }
+
+ /**
* Adds a type converter instance to this database.
*
* @param typeConverter The converter. It must be an instance of a class annotated with
@@ -1149,8 +1172,12 @@
}
}
+ SupportSQLiteOpenHelper.Factory factory;
+
if (mFactory == null) {
- mFactory = new FrameworkSQLiteOpenHelperFactory();
+ factory = new FrameworkSQLiteOpenHelperFactory();
+ } else {
+ factory = mFactory;
}
if (mCopyFromAssetPath != null
@@ -1170,14 +1197,20 @@
+ "Builder, but the database can only be created using one of the "
+ "three configurations.");
}
- mFactory = new SQLiteCopyOpenHelperFactory(mCopyFromAssetPath, mCopyFromFile,
- mCopyFromInputStream, mFactory);
+ factory = new SQLiteCopyOpenHelperFactory(mCopyFromAssetPath, mCopyFromFile,
+ mCopyFromInputStream, factory);
}
+
+ if (mQueryCallback != null) {
+ factory = new QueryInterceptorOpenHelperFactory(factory, mQueryCallback,
+ mQueryCallbackExecutor);
+ }
+
DatabaseConfiguration configuration =
new DatabaseConfiguration(
mContext,
mName,
- mFactory,
+ factory,
mMigrationContainer,
mCallbacks,
mAllowMainThreadQueries,
@@ -1345,4 +1378,21 @@
public void onOpenPrepackagedDatabase(@NonNull SupportSQLiteDatabase db) {
}
}
+
+ /**
+ * Callback interface for when SQLite queries are executed.
+ *
+ * @see RoomDatabase.Builder#setQueryCallback
+ */
+ public interface QueryCallback {
+
+ /**
+ * Called when a SQL query is executed.
+ *
+ * @param sqlQuery The SQLite query statement.
+ * @param bindArgs Arguments of the query if available, empty list otherwise.
+ */
+ void onQuery(@NonNull String sqlQuery, @NonNull List<Object>
+ bindArgs);
+ }
}
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/WindowInsetsPlayground.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/WindowInsetsPlayground.java
index 150e7a9..015d195 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/WindowInsetsPlayground.java
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/WindowInsetsPlayground.java
@@ -20,10 +20,12 @@
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
import android.app.Activity;
+import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.text.Html;
import android.view.View;
+import android.widget.Button;
import android.widget.TextView;
import android.widget.ToggleButton;
@@ -35,6 +37,7 @@
import androidx.core.view.WindowInsetsCompat;
import com.example.android.supportv4.R;
+import com.example.android.supportv4.graphics.DrawableCompatActivity;
@SuppressWarnings("deprecation")
public class WindowInsetsPlayground extends Activity {
@@ -69,6 +72,10 @@
getWindow().setStatusBarColor(0x80000000);
getWindow().setNavigationBarColor(0x80000000);
}
+
+ Button newAct = findViewById(R.id.newAct);
+ newAct.setOnClickListener(
+ v -> startActivity(new Intent(this, DrawableCompatActivity.class)));
}
private void setRootWindowInsetsEnabled(boolean enabled) {
diff --git a/samples/Support4Demos/src/main/res/layout/activity_insets_playground.xml b/samples/Support4Demos/src/main/res/layout/activity_insets_playground.xml
index 3fdb522..d62dbbe 100644
--- a/samples/Support4Demos/src/main/res/layout/activity_insets_playground.xml
+++ b/samples/Support4Demos/src/main/res/layout/activity_insets_playground.xml
@@ -15,11 +15,13 @@
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/insets_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
- android:clipToPadding="false">
+ android:clipToPadding="false"
+ tools:ignore="HardcodedText">
<LinearLayout
android:layout_width="match_parent"
@@ -52,6 +54,12 @@
android:textOn="View insets"
android:textOff="Root window insets" />
+ <Button
+ android:id="@+id/newAct"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="New Act" />
+
</LinearLayout>
<Space
diff --git a/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedSharedPreferencesTest.java b/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedSharedPreferencesTest.java
index d16de8c..0a76af6 100644
--- a/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedSharedPreferencesTest.java
+++ b/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedSharedPreferencesTest.java
@@ -406,4 +406,32 @@
testValue);
}
+ @Test
+ public void testReentrantCallbackCalls() throws Exception {
+ SharedPreferences encryptedSharedPreferences = EncryptedSharedPreferences
+ .create(mContext,
+ PREFS_FILE,
+ mMasterKey,
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM);
+
+ encryptedSharedPreferences.registerOnSharedPreferenceChangeListener(
+ new SharedPreferences.OnSharedPreferenceChangeListener() {
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+ String key) {
+ sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
+ }
+ });
+
+ encryptedSharedPreferences.registerOnSharedPreferenceChangeListener(
+ (sharedPreferences, key) -> {
+ // No-op
+ });
+
+ SharedPreferences.Editor editor = encryptedSharedPreferences.edit();
+ editor.putString("someKey", "someValue");
+ editor.apply();
+ }
+
}
diff --git a/security/crypto/src/main/java/androidx/security/crypto/EncryptedSharedPreferences.java b/security/crypto/src/main/java/androidx/security/crypto/EncryptedSharedPreferences.java
index 6a231a6..44b327f 100644
--- a/security/crypto/src/main/java/androidx/security/crypto/EncryptedSharedPreferences.java
+++ b/security/crypto/src/main/java/androidx/security/crypto/EncryptedSharedPreferences.java
@@ -78,7 +78,7 @@
private static final String NULL_VALUE = "__NULL__";
final SharedPreferences mSharedPreferences;
- final List<OnSharedPreferenceChangeListener> mListeners;
+ final CopyOnWriteArrayList<OnSharedPreferenceChangeListener> mListeners;
final String mFileName;
final String mMasterKeyAlias;
@@ -95,7 +95,7 @@
mMasterKeyAlias = masterKeyAlias;
mValueAead = aead;
mKeyDeterministicAead = deterministicAead;
- mListeners = new ArrayList<>();
+ mListeners = new CopyOnWriteArrayList<>();
}
/**
diff --git a/security/security-app-authenticator/OWNERS b/security/security-app-authenticator/OWNERS
new file mode 100644
index 0000000..fd24685
--- /dev/null
+++ b/security/security-app-authenticator/OWNERS
@@ -0,0 +1 @@
+mpgroover@google.com
\ No newline at end of file
diff --git a/security/security-app-authenticator/api/current.txt b/security/security-app-authenticator/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/security/security-app-authenticator/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/security/security-app-authenticator/api/public_plus_experimental_current.txt b/security/security-app-authenticator/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/security/security-app-authenticator/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/compose/ui/ui-test-font/src/main/res/font/invalid_font.ttf b/security/security-app-authenticator/api/res-current.txt
similarity index 100%
copy from compose/ui/ui-test-font/src/main/res/font/invalid_font.ttf
copy to security/security-app-authenticator/api/res-current.txt
diff --git a/security/security-app-authenticator/api/restricted_current.txt b/security/security-app-authenticator/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/security/security-app-authenticator/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/security/security-app-authenticator/build.gradle b/security/security-app-authenticator/build.gradle
new file mode 100644
index 0000000..60e92d2
--- /dev/null
+++ b/security/security-app-authenticator/build.gradle
@@ -0,0 +1,37 @@
+/*
+ * 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.LibraryVersions
+import androidx.build.LibraryType
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+}
+
+dependencies {
+ // Add dependencies here
+}
+
+androidx {
+ name = "Android Security App Package Authenitcator Library"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenVersion = LibraryVersions.SECURITY_APP_AUTHENTICATOR
+ mavenGroup = LibraryGroups.SECURITY
+ inceptionYear = "2020"
+ description = "Verify app packages for proper app to app authentication."
+}
diff --git a/car/app/app/src/androidTest/AndroidManifest.xml b/security/security-app-authenticator/src/androidTest/AndroidManifest.xml
similarity index 92%
copy from car/app/app/src/androidTest/AndroidManifest.xml
copy to security/security-app-authenticator/src/androidTest/AndroidManifest.xml
index 3bc2684..1ae9328 100644
--- a/car/app/app/src/androidTest/AndroidManifest.xml
+++ b/security/security-app-authenticator/src/androidTest/AndroidManifest.xml
@@ -15,5 +15,6 @@
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="androidx.car.app">
+ package="androidx.security.app.authenticator.test">
+
</manifest>
diff --git a/car/app/app/src/androidTest/AndroidManifest.xml b/security/security-app-authenticator/src/main/AndroidManifest.xml
similarity index 92%
copy from car/app/app/src/androidTest/AndroidManifest.xml
copy to security/security-app-authenticator/src/main/AndroidManifest.xml
index 3bc2684..239ae64 100644
--- a/car/app/app/src/androidTest/AndroidManifest.xml
+++ b/security/security-app-authenticator/src/main/AndroidManifest.xml
@@ -15,5 +15,6 @@
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="androidx.car.app">
-</manifest>
+ package="androidx.security.app.authenticator">
+
+</manifest>
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index a04f8c7..661f378 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -432,6 +432,7 @@
includeProject(":room:room-testing", "room/testing", [BuildType.MAIN])
includeProject(":savedstate:savedstate", "savedstate/savedstate", [BuildType.MAIN, BuildType.FLAN])
includeProject(":savedstate:savedstate-ktx", "savedstate/savedstate-ktx", [BuildType.MAIN, BuildType.FLAN])
+includeProject(":security:security-app-authenticator", "security/security-app-authenticator", [BuildType.MAIN])
includeProject(":security:security-biometric", "security/security-biometric", [BuildType.MAIN])
includeProject(":security:security-crypto", "security/crypto", [BuildType.MAIN])
includeProject(":security:security-crypto-ktx", "security/security-crypto-ktx", [BuildType.MAIN])
diff --git a/slices/view/lint-baseline.xml b/slices/view/lint-baseline.xml
index 1199cdc7..999809f 100644
--- a/slices/view/lint-baseline.xml
+++ b/slices/view/lint-baseline.xml
@@ -1982,501 +1982,6 @@
</issue>
<issue
- id="UnusedQuantity"
- message="For language "cs" (Czech) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-cs/strings.xml"
- line="24"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "in" (Indonesian) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-in/strings.xml"
- line="24"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ja" (Japanese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ja/strings.xml"
- line="24"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "km" (Khmer) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-km/strings.xml"
- line="24"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ko" (Korean) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ko/strings.xml"
- line="24"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "lo" (Lao) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-lo/strings.xml"
- line="24"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "lt" (Lithuanian) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-lt/strings.xml"
- line="24"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ms" (Malay) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ms/strings.xml"
- line="24"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "my" (Burmese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-my/strings.xml"
- line="24"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "sk" (Slovak) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-sk/strings.xml"
- line="24"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "th" (Thai) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-th/strings.xml"
- line="24"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "vi" (Vietnamese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-vi/strings.xml"
- line="24"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rCN/strings.xml"
- line="24"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rHK/strings.xml"
- line="24"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rTW/strings.xml"
- line="24"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "in" (Indonesian) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-in/strings.xml"
- line="28"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ja" (Japanese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ja/strings.xml"
- line="28"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "km" (Khmer) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-km/strings.xml"
- line="28"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ko" (Korean) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ko/strings.xml"
- line="28"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "lo" (Lao) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-lo/strings.xml"
- line="28"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ms" (Malay) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ms/strings.xml"
- line="28"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "my" (Burmese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-my/strings.xml"
- line="28"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "th" (Thai) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-th/strings.xml"
- line="28"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "vi" (Vietnamese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-vi/strings.xml"
- line="28"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rCN/strings.xml"
- line="28"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rHK/strings.xml"
- line="28"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rTW/strings.xml"
- line="28"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "cs" (Czech) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-cs/strings.xml"
- line="30"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "lt" (Lithuanian) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-lt/strings.xml"
- line="30"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "sk" (Slovak) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="abc_slice_duration_years" formatted="false" msgid="6212691832333991589">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-sk/strings.xml"
- line="30"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "in" (Indonesian) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-in/strings.xml"
- line="32"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ja" (Japanese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ja/strings.xml"
- line="32"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "km" (Khmer) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-km/strings.xml"
- line="32"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ko" (Korean) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ko/strings.xml"
- line="32"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "lo" (Lao) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-lo/strings.xml"
- line="32"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ms" (Malay) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ms/strings.xml"
- line="32"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "my" (Burmese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-my/strings.xml"
- line="32"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "th" (Thai) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-th/strings.xml"
- line="32"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "vi" (Vietnamese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-vi/strings.xml"
- line="32"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rCN/strings.xml"
- line="32"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rHK/strings.xml"
- line="32"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rTW/strings.xml"
- line="32"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "cs" (Czech) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-cs/strings.xml"
- line="36"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "lt" (Lithuanian) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-lt/strings.xml"
- line="36"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "sk" (Slovak) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="abc_slice_duration_days" formatted="false" msgid="6241698511167107334">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-sk/strings.xml"
- line="36"
- column="5"/>
- </issue>
-
- <issue
id="ObsoleteLayoutParam"
message="Invalid layout param in a `LinearLayout`: `layout_alignStart`"
errorLine1=" android:layout_alignStart="@android:id/title""
diff --git a/slices/view/src/androidTest/java/androidx/slice/widget/SliceStyleTest.java b/slices/view/src/androidTest/java/androidx/slice/widget/SliceStyleTest.java
index 3f98f01..8a2b761 100644
--- a/slices/view/src/androidTest/java/androidx/slice/widget/SliceStyleTest.java
+++ b/slices/view/src/androidTest/java/androidx/slice/widget/SliceStyleTest.java
@@ -48,6 +48,7 @@
@Before
public void setup() {
+ mContext.setTheme(R.style.AppTheme);
// Empty XML file to initialize empty AttributeSet.
XmlPullParser parser = mContext.getResources().getXml(R.xml.slice_style_test);
AttributeSet attributes = Xml.asAttributeSet(parser);
diff --git a/slices/view/src/androidTest/java/androidx/slice/widget/SliceViewTest.java b/slices/view/src/androidTest/java/androidx/slice/widget/SliceViewTest.java
index f9dcce2..4fc92c0 100644
--- a/slices/view/src/androidTest/java/androidx/slice/widget/SliceViewTest.java
+++ b/slices/view/src/androidTest/java/androidx/slice/widget/SliceViewTest.java
@@ -74,6 +74,7 @@
@Before
@UiThreadTest
public void setup() {
+ mContext.setTheme(R.style.AppTheme);
mSliceView = new SliceView(mContext);
SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS);
}
diff --git a/studiow b/studiow
index 577071b..2dc12360 100755
--- a/studiow
+++ b/studiow
@@ -1,16 +1,48 @@
#!/usr/bin/env bash
-if [ -n "$1" ]; then
- export ANDROIDX_PROJECTS=${1^^}
-else
- export ANDROIDX_PROJECTS=MAIN
- echo "Supported projects sets include:"
- echo "- MAIN for non-Compose Jetpack libraries"
- echo "- COMPOSE for Compose and dependencies"
- echo "- FLAN for Fragment, Lifecycle, Activity, and Navigation"
- echo "- ALL for all libraries"
- echo
- echo "No project set specified, using MAIN..."
-fi
-shift
-source gradlew studio "$@"
+function usage() {
+ echo "Usage: studiow [<project subset>]"
+ echo
+ echo "Project subsets:"
+ echo " m, main"
+ echo " Open the project subset MAIN: non-Compose Jetpack libraries"
+ echo
+ echo " c, compose"
+ echo " Open the project subset COMPOSE"
+ echo
+ echo " f, flan"
+ echo " Open the project subset FLAN: Fragment, Lifecycle, Activity, and Navigation"
+ echo
+ echo " a, all"
+ echo " Open the project subset ALL"
+ echo
+ exit 1
+}
+
+subsetArg="$1"
+if [ "$subsetArg" == "" ]; then
+ usage
+fi
+if [ "$subsetArg" == "m" -o "$subsetArg" == "main" ]; then
+ export ANDROIDX_PROJECTS=MAIN
+fi
+if [ "$subsetArg" == "c" -o "$subsetArg" == "compose" ]; then
+ export ANDROIDX_PROJECTS=COMPOSE
+fi
+if [ "$subsetArg" == "f" -o "$subsetArg" == "flan" ]; then
+ export ANDROIDX_PROJECTS=FLAN
+fi
+if [ "$subsetArg" == "a" -o "$subsetArg" == "all" ]; then
+ export ANDROIDX_PROJECTS=ALL
+fi
+if [ "$ANDROIDX_PROJECTS" == "" ]; then
+ echo "Unrecognized project argument: '$subsetArg'"
+ usage
+fi
+
+shift
+if [ "$1" != "" ]; then
+ echo "Unrecognized argument: '$1'"
+ usage
+fi
+source gradlew studio
diff --git a/testutils/testutils-common/src/main/java/androidx/testutils/TestExecutor.kt b/testutils/testutils-common/src/main/java/androidx/testutils/TestExecutor.kt
index f1730b0..f1ac548 100644
--- a/testutils/testutils-common/src/main/java/androidx/testutils/TestExecutor.kt
+++ b/testutils/testutils-common/src/main/java/androidx/testutils/TestExecutor.kt
@@ -20,10 +20,18 @@
import java.util.concurrent.Executor
class TestExecutor : Executor {
+ /**
+ * If true, adding a new task will drain all existing tasks.
+ */
+ var autoRun: Boolean = false
+
private val mTasks = LinkedList<Runnable>()
override fun execute(command: Runnable) {
mTasks.add(command)
+ if (autoRun) {
+ executeAll()
+ }
}
fun executeAll(): Boolean {
diff --git a/testutils/testutils-paging/src/main/java/androidx/paging/LoadStateUtils.kt b/testutils/testutils-paging/src/main/java/androidx/paging/LoadStateUtils.kt
index 6e22616..76a4482 100644
--- a/testutils/testutils-paging/src/main/java/androidx/paging/LoadStateUtils.kt
+++ b/testutils/testutils-paging/src/main/java/androidx/paging/LoadStateUtils.kt
@@ -19,23 +19,6 @@
import androidx.paging.LoadState.NotLoading
/**
- * Converts a list of incremental LoadState updates to local source to a list of expected
- * [CombinedLoadStates] events.
- */
-@OptIn(ExperimentalStdlibApi::class)
-fun List<Pair<LoadType, LoadState>>.toCombinedLoadStatesLocal() = scan(
- CombinedLoadStates(
- source = LoadStates(
- refresh = NotLoading(endOfPaginationReached = false),
- prepend = NotLoading(endOfPaginationReached = false),
- append = NotLoading(endOfPaginationReached = false)
- )
- )
-) { prev, update ->
- prev.set(update.first, false, update.second)
-}
-
-/**
* Test-only local-only LoadStates builder which defaults each state to [NotLoading], with
* [LoadState.endOfPaginationReached] = `false`
*/
@@ -44,11 +27,14 @@
prependLocal: LoadState = NotLoading(endOfPaginationReached = false),
appendLocal: LoadState = NotLoading(endOfPaginationReached = false)
) = CombinedLoadStates(
+ refresh = refreshLocal,
+ prepend = prependLocal,
+ append = appendLocal,
source = LoadStates(
refresh = refreshLocal,
prepend = prependLocal,
- append = appendLocal
- )
+ append = appendLocal,
+ ),
)
/**
@@ -56,6 +42,9 @@
* [LoadState.endOfPaginationReached] = `false`
*/
fun remoteLoadStatesOf(
+ refresh: LoadState = NotLoading(endOfPaginationReached = false),
+ prepend: LoadState = NotLoading(endOfPaginationReached = false),
+ append: LoadState = NotLoading(endOfPaginationReached = false),
refreshLocal: LoadState = NotLoading(endOfPaginationReached = false),
prependLocal: LoadState = NotLoading(endOfPaginationReached = false),
appendLocal: LoadState = NotLoading(endOfPaginationReached = false),
@@ -63,66 +52,17 @@
prependRemote: LoadState = NotLoading(endOfPaginationReached = false),
appendRemote: LoadState = NotLoading(endOfPaginationReached = false)
) = CombinedLoadStates(
+ refresh = refresh,
+ prepend = prepend,
+ append = append,
source = LoadStates(
refresh = refreshLocal,
prepend = prependLocal,
- append = appendLocal
+ append = appendLocal,
),
mediator = LoadStates(
refresh = refreshRemote,
prepend = prependRemote,
- append = appendRemote
- )
+ append = appendRemote,
+ ),
)
-
-private fun CombinedLoadStates.set(
- loadType: LoadType,
- fromMediator: Boolean,
- loadState: LoadState
-) = when (loadType) {
- LoadType.REFRESH ->
- if (fromMediator) {
- copy(
- mediator = mediator?.copy(refresh = loadState)
- ?: LoadStates(
- refresh = loadState,
- prepend = NotLoading(false),
- append = NotLoading(false)
- )
- )
- } else {
- copy(
- source = source.copy(refresh = loadState)
- )
- }
- LoadType.PREPEND ->
- if (fromMediator) {
- copy(
- mediator = mediator?.copy(prepend = loadState)
- ?: LoadStates(
- refresh = NotLoading(false),
- prepend = loadState,
- append = NotLoading(false)
- )
- )
- } else {
- copy(
- source = source.copy(prepend = loadState)
- )
- }
- LoadType.APPEND ->
- if (fromMediator) {
- copy(
- mediator = mediator?.copy(append = loadState)
- ?: LoadStates(
- refresh = NotLoading(false),
- prepend = NotLoading(false),
- append = loadState
- )
- )
- } else {
- copy(
- source = source.copy(append = loadState)
- )
- }
-}
diff --git a/testutils/testutils-paging/src/main/java/androidx/paging/TestPagingDataDiffer.kt b/testutils/testutils-paging/src/main/java/androidx/paging/TestPagingDataDiffer.kt
index 7c329b9..014dae3 100644
--- a/testutils/testutils-paging/src/main/java/androidx/paging/TestPagingDataDiffer.kt
+++ b/testutils/testutils-paging/src/main/java/androidx/paging/TestPagingDataDiffer.kt
@@ -28,8 +28,12 @@
previousList: NullPaddedList<T>,
newList: NullPaddedList<T>,
newCombinedLoadStates: CombinedLoadStates,
- lastAccessedIndex: Int
- ): Int? = null
+ lastAccessedIndex: Int,
+ onListPresentable: () -> Unit,
+ ): Int? {
+ onListPresentable()
+ return null
+ }
companion object {
private val noopDifferCallback = object : DifferCallback {
diff --git a/testutils/testutils-paging/src/main/java/androidx/paging/TestPagingSource.kt b/testutils/testutils-paging/src/main/java/androidx/paging/TestPagingSource.kt
index d0113b5..d5f3b92 100644
--- a/testutils/testutils-paging/src/main/java/androidx/paging/TestPagingSource.kt
+++ b/testutils/testutils-paging/src/main/java/androidx/paging/TestPagingSource.kt
@@ -82,7 +82,6 @@
}
}
- @OptIn(ExperimentalPagingApi::class)
override fun getRefreshKey(state: PagingState<Int, Int>): Int? {
getRefreshKeyCalls.add(state)
return state.anchorPosition
diff --git a/testutils/testutils-truth/src/main/java/androidx/testutils/assertions.kt b/testutils/testutils-truth/src/main/java/androidx/testutils/assertions.kt
index 6e86816..1dfc738 100644
--- a/testutils/testutils-truth/src/main/java/androidx/testutils/assertions.kt
+++ b/testutils/testutils-truth/src/main/java/androidx/testutils/assertions.kt
@@ -46,3 +46,22 @@
}
fun fail(message: String? = null): Nothing = throw AssertionError(message)
+
+// The assertThrows above cannot be used from Java.
+@Suppress("UNCHECKED_CAST")
+fun <T : Throwable?> assertThrows(
+ expectedType: Class<T>,
+ runnable: Runnable
+): ThrowableSubject {
+ try {
+ runnable.run()
+ } catch (t: Throwable) {
+ if (expectedType.isInstance(t)) {
+ return assertThat(t)
+ }
+ throw t
+ }
+ throw AssertionError(
+ "Body completed successfully. Expected ${expectedType.simpleName}"
+ )
+}
\ No newline at end of file
diff --git a/testutils/testutils-truth/src/test/java/androidx/testutils/AssertionsTest.kt b/testutils/testutils-truth/src/test/java/androidx/testutils/AssertionsTest.kt
new file mode 100644
index 0000000..b65e977
--- /dev/null
+++ b/testutils/testutils-truth/src/test/java/androidx/testutils/AssertionsTest.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.testutils
+
+import org.junit.Test
+import java.io.IOException
+
+class AssertionsTest {
+ @Test
+ fun testNoFailureThrowsAssertionError() {
+ try {
+ assertThrows(IOException::class.java) {
+ // No Exception thrown
+ }
+ } catch (e: AssertionError) {
+ return // expected
+ }
+
+ fail("expected assertion error for no failure")
+ }
+
+ @Test
+ fun testIncorrectFailureThrowsAssertionError() {
+ try {
+ assertThrows(IOException::class.java) {
+ throw IllegalStateException()
+ }
+ } catch (e: IllegalStateException) {
+ return // expected
+ }
+
+ fail("expected IllegalStateException to propagate")
+ }
+
+ @Test
+ fun testCorrectFailureTypeIsCaughtAndReturnsAsThrowableSubject() {
+ assertThrows(IOException::class.java) {
+ throw IOException("test123")
+ }.hasMessageThat().contains("test123")
+ }
+}
\ No newline at end of file
diff --git a/wear/wear-complications-data/lint-baseline.xml b/wear/wear-complications-data/lint-baseline.xml
index d0c3c4b..dde667a 100644
--- a/wear/wear-complications-data/lint-baseline.xml
+++ b/wear/wear-complications-data/lint-baseline.xml
@@ -45,994 +45,4 @@
column="1"/>
</issue>
- <issue
- id="UnusedQuantity"
- message="For language "cs" (Czech) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="time_difference_short_days" formatted="false" msgid="8500262093840795448">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-cs/complication_strings.xml"
- line="4"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "in" (Indonesian) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_days" formatted="false" msgid="8500262093840795448">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-in/complication_strings.xml"
- line="4"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ja" (Japanese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_days" formatted="false" msgid="8500262093840795448">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ja/complication_strings.xml"
- line="4"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "km" (Khmer) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_days" formatted="false" msgid="8500262093840795448">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-km/complication_strings.xml"
- line="4"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ko" (Korean) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_days" formatted="false" msgid="8500262093840795448">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ko/complication_strings.xml"
- line="4"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "lo" (Lao) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_days" formatted="false" msgid="8500262093840795448">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-lo/complication_strings.xml"
- line="4"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "lt" (Lithuanian) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="time_difference_short_days" formatted="false" msgid="8500262093840795448">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-lt/complication_strings.xml"
- line="4"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ms" (Malay) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_days" formatted="false" msgid="8500262093840795448">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ms/complication_strings.xml"
- line="4"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "my" (Burmese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_days" formatted="false" msgid="8500262093840795448">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-my/complication_strings.xml"
- line="4"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "sk" (Slovak) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="time_difference_short_days" formatted="false" msgid="8500262093840795448">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-sk/complication_strings.xml"
- line="4"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "th" (Thai) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_days" formatted="false" msgid="8500262093840795448">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-th/complication_strings.xml"
- line="4"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "vi" (Vietnamese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_days" formatted="false" msgid="8500262093840795448">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-vi/complication_strings.xml"
- line="4"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_days" formatted="false" msgid="8500262093840795448">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rCN/complication_strings.xml"
- line="4"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_days" formatted="false" msgid="8500262093840795448">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rHK/complication_strings.xml"
- line="4"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_days" formatted="false" msgid="8500262093840795448">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rTW/complication_strings.xml"
- line="4"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "in" (Indonesian) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_hours" formatted="false" msgid="3258361256003469346">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-in/complication_strings.xml"
- line="8"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ja" (Japanese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_hours" formatted="false" msgid="3258361256003469346">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ja/complication_strings.xml"
- line="8"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "km" (Khmer) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_hours" formatted="false" msgid="3258361256003469346">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-km/complication_strings.xml"
- line="8"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ko" (Korean) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_hours" formatted="false" msgid="3258361256003469346">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ko/complication_strings.xml"
- line="8"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "lo" (Lao) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_hours" formatted="false" msgid="3258361256003469346">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-lo/complication_strings.xml"
- line="8"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ms" (Malay) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_hours" formatted="false" msgid="3258361256003469346">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ms/complication_strings.xml"
- line="8"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "my" (Burmese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_hours" formatted="false" msgid="3258361256003469346">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-my/complication_strings.xml"
- line="8"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "th" (Thai) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_hours" formatted="false" msgid="3258361256003469346">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-th/complication_strings.xml"
- line="8"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "vi" (Vietnamese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_hours" formatted="false" msgid="3258361256003469346">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-vi/complication_strings.xml"
- line="8"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_hours" formatted="false" msgid="3258361256003469346">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rCN/complication_strings.xml"
- line="8"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_hours" formatted="false" msgid="3258361256003469346">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rHK/complication_strings.xml"
- line="8"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_hours" formatted="false" msgid="3258361256003469346">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rTW/complication_strings.xml"
- line="8"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "cs" (Czech) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="time_difference_short_hours" formatted="false" msgid="3258361256003469346">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-cs/complication_strings.xml"
- line="10"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "lt" (Lithuanian) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="time_difference_short_hours" formatted="false" msgid="3258361256003469346">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-lt/complication_strings.xml"
- line="10"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "sk" (Slovak) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="time_difference_short_hours" formatted="false" msgid="3258361256003469346">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-sk/complication_strings.xml"
- line="10"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "in" (Indonesian) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_minutes" formatted="false" msgid="3812930575997556650">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-in/complication_strings.xml"
- line="12"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ja" (Japanese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_minutes" formatted="false" msgid="3812930575997556650">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ja/complication_strings.xml"
- line="12"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "km" (Khmer) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_minutes" formatted="false" msgid="3812930575997556650">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-km/complication_strings.xml"
- line="12"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ko" (Korean) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_minutes" formatted="false" msgid="3812930575997556650">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ko/complication_strings.xml"
- line="12"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "lo" (Lao) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_minutes" formatted="false" msgid="3812930575997556650">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-lo/complication_strings.xml"
- line="12"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ms" (Malay) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_minutes" formatted="false" msgid="3812930575997556650">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ms/complication_strings.xml"
- line="12"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "my" (Burmese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_minutes" formatted="false" msgid="3812930575997556650">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-my/complication_strings.xml"
- line="12"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "th" (Thai) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_minutes" formatted="false" msgid="3812930575997556650">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-th/complication_strings.xml"
- line="12"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "vi" (Vietnamese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_minutes" formatted="false" msgid="3812930575997556650">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-vi/complication_strings.xml"
- line="12"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_minutes" formatted="false" msgid="3812930575997556650">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rCN/complication_strings.xml"
- line="12"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_minutes" formatted="false" msgid="3812930575997556650">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rHK/complication_strings.xml"
- line="12"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_short_minutes" formatted="false" msgid="3812930575997556650">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rTW/complication_strings.xml"
- line="12"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "cs" (Czech) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="time_difference_short_minutes" formatted="false" msgid="3812930575997556650">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-cs/complication_strings.xml"
- line="16"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "lt" (Lithuanian) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="time_difference_short_minutes" formatted="false" msgid="3812930575997556650">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-lt/complication_strings.xml"
- line="16"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "sk" (Slovak) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="time_difference_short_minutes" formatted="false" msgid="3812930575997556650">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-sk/complication_strings.xml"
- line="16"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "in" (Indonesian) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_days" formatted="false" msgid="345557497041553025">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-in/complication_strings.xml"
- line="18"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ja" (Japanese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_days" formatted="false" msgid="345557497041553025">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ja/complication_strings.xml"
- line="18"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "km" (Khmer) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_days" formatted="false" msgid="345557497041553025">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-km/complication_strings.xml"
- line="18"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ko" (Korean) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_days" formatted="false" msgid="345557497041553025">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ko/complication_strings.xml"
- line="18"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "lo" (Lao) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_days" formatted="false" msgid="345557497041553025">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-lo/complication_strings.xml"
- line="18"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ms" (Malay) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_days" formatted="false" msgid="345557497041553025">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ms/complication_strings.xml"
- line="18"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "my" (Burmese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_days" formatted="false" msgid="345557497041553025">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-my/complication_strings.xml"
- line="18"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "th" (Thai) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_days" formatted="false" msgid="345557497041553025">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-th/complication_strings.xml"
- line="18"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "vi" (Vietnamese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_days" formatted="false" msgid="345557497041553025">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-vi/complication_strings.xml"
- line="18"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_days" formatted="false" msgid="345557497041553025">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rCN/complication_strings.xml"
- line="18"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_days" formatted="false" msgid="345557497041553025">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rHK/complication_strings.xml"
- line="18"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_days" formatted="false" msgid="345557497041553025">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rTW/complication_strings.xml"
- line="18"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "in" (Indonesian) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_hours" formatted="false" msgid="2990178439049007198">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-in/complication_strings.xml"
- line="22"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ja" (Japanese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_hours" formatted="false" msgid="2990178439049007198">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ja/complication_strings.xml"
- line="22"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "km" (Khmer) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_hours" formatted="false" msgid="2990178439049007198">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-km/complication_strings.xml"
- line="22"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ko" (Korean) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_hours" formatted="false" msgid="2990178439049007198">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ko/complication_strings.xml"
- line="22"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "lo" (Lao) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_hours" formatted="false" msgid="2990178439049007198">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-lo/complication_strings.xml"
- line="22"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ms" (Malay) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_hours" formatted="false" msgid="2990178439049007198">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ms/complication_strings.xml"
- line="22"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "my" (Burmese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_hours" formatted="false" msgid="2990178439049007198">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-my/complication_strings.xml"
- line="22"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "th" (Thai) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_hours" formatted="false" msgid="2990178439049007198">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-th/complication_strings.xml"
- line="22"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "vi" (Vietnamese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_hours" formatted="false" msgid="2990178439049007198">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-vi/complication_strings.xml"
- line="22"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_hours" formatted="false" msgid="2990178439049007198">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rCN/complication_strings.xml"
- line="22"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_hours" formatted="false" msgid="2990178439049007198">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rHK/complication_strings.xml"
- line="22"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_hours" formatted="false" msgid="2990178439049007198">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rTW/complication_strings.xml"
- line="22"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "cs" (Czech) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="time_difference_words_days" formatted="false" msgid="345557497041553025">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-cs/complication_strings.xml"
- line="24"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "lt" (Lithuanian) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="time_difference_words_days" formatted="false" msgid="345557497041553025">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-lt/complication_strings.xml"
- line="24"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "sk" (Slovak) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="time_difference_words_days" formatted="false" msgid="345557497041553025">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-sk/complication_strings.xml"
- line="24"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "in" (Indonesian) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_minutes" formatted="false" msgid="9081188175463984403">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-in/complication_strings.xml"
- line="26"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ja" (Japanese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_minutes" formatted="false" msgid="9081188175463984403">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ja/complication_strings.xml"
- line="26"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "km" (Khmer) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_minutes" formatted="false" msgid="9081188175463984403">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-km/complication_strings.xml"
- line="26"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ko" (Korean) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_minutes" formatted="false" msgid="9081188175463984403">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ko/complication_strings.xml"
- line="26"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "lo" (Lao) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_minutes" formatted="false" msgid="9081188175463984403">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-lo/complication_strings.xml"
- line="26"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "ms" (Malay) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_minutes" formatted="false" msgid="9081188175463984403">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-ms/complication_strings.xml"
- line="26"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "my" (Burmese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_minutes" formatted="false" msgid="9081188175463984403">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-my/complication_strings.xml"
- line="26"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "th" (Thai) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_minutes" formatted="false" msgid="9081188175463984403">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-th/complication_strings.xml"
- line="26"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "vi" (Vietnamese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_minutes" formatted="false" msgid="9081188175463984403">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-vi/complication_strings.xml"
- line="26"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_minutes" formatted="false" msgid="9081188175463984403">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rCN/complication_strings.xml"
- line="26"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_minutes" formatted="false" msgid="9081188175463984403">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rHK/complication_strings.xml"
- line="26"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "zh" (Chinese) the following quantities are not relevant: `one`"
- errorLine1=" <plurals name="time_difference_words_minutes" formatted="false" msgid="9081188175463984403">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-zh-rTW/complication_strings.xml"
- line="26"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "cs" (Czech) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="time_difference_words_hours" formatted="false" msgid="2990178439049007198">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-cs/complication_strings.xml"
- line="30"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "lt" (Lithuanian) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="time_difference_words_hours" formatted="false" msgid="2990178439049007198">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-lt/complication_strings.xml"
- line="30"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "sk" (Slovak) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="time_difference_words_hours" formatted="false" msgid="2990178439049007198">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-sk/complication_strings.xml"
- line="30"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "cs" (Czech) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="time_difference_words_minutes" formatted="false" msgid="9081188175463984403">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-cs/complication_strings.xml"
- line="36"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "lt" (Lithuanian) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="time_difference_words_minutes" formatted="false" msgid="9081188175463984403">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-lt/complication_strings.xml"
- line="36"
- column="5"/>
- </issue>
-
- <issue
- id="UnusedQuantity"
- message="For language "sk" (Slovak) the following quantities are not relevant: `many`"
- errorLine1=" <plurals name="time_difference_words_minutes" formatted="false" msgid="9081188175463984403">"
- errorLine2=" ^">
- <location
- file="src/main/res/values-sk/complication_strings.xml"
- line="36"
- column="5"/>
- </issue>
-
</issues>
diff --git a/wear/wear-watchface-client/api/current.txt b/wear/wear-watchface-client/api/current.txt
index ee7fa64..c744f24 100644
--- a/wear/wear-watchface-client/api/current.txt
+++ b/wear/wear-watchface-client/api/current.txt
@@ -2,15 +2,17 @@
package androidx.wear.watchface.client {
public final class ComplicationState {
- ctor public ComplicationState(android.graphics.Rect bounds, int boundsType, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.data.ComplicationType defaultProviderType, boolean isEnabled);
+ ctor public ComplicationState(android.graphics.Rect bounds, int boundsType, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.data.ComplicationType defaultProviderType, boolean isEnabled, androidx.wear.complications.data.ComplicationType currentType);
method public android.graphics.Rect getBounds();
method public int getBoundsType();
+ method public androidx.wear.complications.data.ComplicationType getCurrentType();
method public androidx.wear.complications.DefaultComplicationProviderPolicy getDefaultProviderPolicy();
method public androidx.wear.complications.data.ComplicationType getDefaultProviderType();
method public java.util.List<androidx.wear.complications.data.ComplicationType> getSupportedTypes();
method public boolean isEnabled();
property public final android.graphics.Rect bounds;
property public final int boundsType;
+ property public final androidx.wear.complications.data.ComplicationType currentType;
property public final androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy;
property public final androidx.wear.complications.data.ComplicationType defaultProviderType;
property public final boolean isEnabled;
@@ -84,7 +86,9 @@
public interface InteractiveWatchFaceWcsClient extends java.lang.AutoCloseable {
method public android.os.IBinder asBinder();
+ method public void bringAttentionToComplication(int complicationId);
method public default static androidx.wear.watchface.client.InteractiveWatchFaceWcsClient createFromBinder(android.os.IBinder binder);
+ method public default Integer? getComplicationIdAt(@Px int x, @Px int y);
method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
method public String getInstanceId();
method public long getPreviewReferenceTimeMillis();
diff --git a/wear/wear-watchface-client/api/public_plus_experimental_current.txt b/wear/wear-watchface-client/api/public_plus_experimental_current.txt
index 5b9d604..3e7223f 100644
--- a/wear/wear-watchface-client/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface-client/api/public_plus_experimental_current.txt
@@ -2,15 +2,17 @@
package androidx.wear.watchface.client {
public final class ComplicationState {
- ctor public ComplicationState(android.graphics.Rect bounds, @androidx.wear.watchface.data.ComplicationBoundsType int boundsType, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.data.ComplicationType defaultProviderType, boolean isEnabled);
+ ctor public ComplicationState(android.graphics.Rect bounds, @androidx.wear.watchface.data.ComplicationBoundsType int boundsType, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.data.ComplicationType defaultProviderType, boolean isEnabled, androidx.wear.complications.data.ComplicationType currentType);
method public android.graphics.Rect getBounds();
method public int getBoundsType();
+ method public androidx.wear.complications.data.ComplicationType getCurrentType();
method public androidx.wear.complications.DefaultComplicationProviderPolicy getDefaultProviderPolicy();
method public androidx.wear.complications.data.ComplicationType getDefaultProviderType();
method public java.util.List<androidx.wear.complications.data.ComplicationType> getSupportedTypes();
method public boolean isEnabled();
property public final android.graphics.Rect bounds;
property public final int boundsType;
+ property public final androidx.wear.complications.data.ComplicationType currentType;
property public final androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy;
property public final androidx.wear.complications.data.ComplicationType defaultProviderType;
property public final boolean isEnabled;
@@ -84,7 +86,9 @@
public interface InteractiveWatchFaceWcsClient extends java.lang.AutoCloseable {
method public android.os.IBinder asBinder();
+ method public void bringAttentionToComplication(int complicationId);
method public default static androidx.wear.watchface.client.InteractiveWatchFaceWcsClient createFromBinder(android.os.IBinder binder);
+ method public default Integer? getComplicationIdAt(@Px int x, @Px int y);
method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
method public String getInstanceId();
method public long getPreviewReferenceTimeMillis();
diff --git a/wear/wear-watchface-client/api/restricted_current.txt b/wear/wear-watchface-client/api/restricted_current.txt
index be317d9..7961108 100644
--- a/wear/wear-watchface-client/api/restricted_current.txt
+++ b/wear/wear-watchface-client/api/restricted_current.txt
@@ -2,15 +2,17 @@
package androidx.wear.watchface.client {
public final class ComplicationState {
- ctor public ComplicationState(android.graphics.Rect bounds, @androidx.wear.watchface.data.ComplicationBoundsType int boundsType, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.data.ComplicationType defaultProviderType, boolean isEnabled);
+ ctor public ComplicationState(android.graphics.Rect bounds, @androidx.wear.watchface.data.ComplicationBoundsType int boundsType, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, androidx.wear.complications.data.ComplicationType defaultProviderType, boolean isEnabled, androidx.wear.complications.data.ComplicationType currentType);
method public android.graphics.Rect getBounds();
method public int getBoundsType();
+ method public androidx.wear.complications.data.ComplicationType getCurrentType();
method public androidx.wear.complications.DefaultComplicationProviderPolicy getDefaultProviderPolicy();
method public androidx.wear.complications.data.ComplicationType getDefaultProviderType();
method public java.util.List<androidx.wear.complications.data.ComplicationType> getSupportedTypes();
method public boolean isEnabled();
property public final android.graphics.Rect bounds;
property public final int boundsType;
+ property public final androidx.wear.complications.data.ComplicationType currentType;
property public final androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy;
property public final androidx.wear.complications.data.ComplicationType defaultProviderType;
property public final boolean isEnabled;
@@ -84,7 +86,9 @@
public interface InteractiveWatchFaceWcsClient extends java.lang.AutoCloseable {
method public android.os.IBinder asBinder();
+ method public void bringAttentionToComplication(int complicationId);
method public default static androidx.wear.watchface.client.InteractiveWatchFaceWcsClient createFromBinder(android.os.IBinder binder);
+ method public default Integer? getComplicationIdAt(@Px int x, @Px int y);
method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
method public String getInstanceId();
method public long getPreviewReferenceTimeMillis();
diff --git a/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index 7a6a332..898a8cfe 100644
--- a/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -19,6 +19,7 @@
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.graphics.Color
import android.graphics.Rect
import android.os.Handler
import android.os.Looper
@@ -32,8 +33,10 @@
import androidx.wear.complications.SystemProviders
import androidx.wear.complications.data.ComplicationText
import androidx.wear.complications.data.ComplicationType
+import androidx.wear.complications.data.LongTextComplicationData
import androidx.wear.complications.data.ShortTextComplicationData
import androidx.wear.watchface.DrawMode
+import androidx.wear.watchface.LayerMode
import androidx.wear.watchface.RenderParameters
import androidx.wear.watchface.client.DeviceConfig
import androidx.wear.watchface.client.SystemState
@@ -50,9 +53,11 @@
import androidx.wear.watchface.samples.GREEN_STYLE
import androidx.wear.watchface.samples.NO_COMPLICATIONS
import androidx.wear.watchface.samples.WATCH_HAND_LENGTH_STYLE_SETTING
+import androidx.wear.watchface.style.Layer
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
@@ -151,7 +156,12 @@
400
).get(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)!!
val bitmap = headlessInstance.takeWatchFaceScreenshot(
- RenderParameters(DrawMode.INTERACTIVE, RenderParameters.DRAW_ALL_LAYERS, null),
+ RenderParameters(
+ DrawMode.INTERACTIVE,
+ RenderParameters.DRAW_ALL_LAYERS,
+ null,
+ Color.RED
+ ),
100,
1234567,
null,
@@ -164,6 +174,44 @@
}
@Test
+ fun yellowComplicationHighlights() {
+ val headlessInstance = service.createHeadlessWatchFaceClient(
+ ComponentName(
+ "androidx.wear.watchface.samples.test",
+ "androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService"
+ ),
+ DeviceConfig(
+ false,
+ false,
+ 0,
+ 0
+ ),
+ 400,
+ 400
+ ).get(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)!!
+ val bitmap = headlessInstance.takeWatchFaceScreenshot(
+ RenderParameters(
+ DrawMode.INTERACTIVE,
+ mapOf(
+ Layer.BASE_LAYER to LayerMode.DRAW,
+ Layer.COMPLICATIONS to LayerMode.DRAW_HIGHLIGHTED,
+ Layer.TOP_LAYER to LayerMode.DRAW
+ ),
+ null,
+ Color.YELLOW
+ ),
+ 100,
+ 1234567,
+ null,
+ complications
+ )
+
+ bitmap.assertAgainstGolden(screenshotRule, "yellowComplicationHighlights")
+
+ headlessInstance.close()
+ }
+
+ @Test
fun headlessComplicationDetails() {
val headlessInstance = service.createHeadlessWatchFaceClient(
exampleWatchFaceComponentName,
@@ -264,7 +312,12 @@
interactiveInstanceFuture.get(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)!!
val bitmap = interactiveInstance.takeWatchFaceScreenshot(
- RenderParameters(DrawMode.INTERACTIVE, RenderParameters.DRAW_ALL_LAYERS, null),
+ RenderParameters(
+ DrawMode.INTERACTIVE,
+ RenderParameters.DRAW_ALL_LAYERS,
+ null,
+ Color.RED
+ ),
100,
1234567,
null,
@@ -304,7 +357,12 @@
interactiveInstanceFuture.get(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)!!
val bitmap = interactiveInstance.takeWatchFaceScreenshot(
- RenderParameters(DrawMode.INTERACTIVE, RenderParameters.DRAW_ALL_LAYERS, null),
+ RenderParameters(
+ DrawMode.INTERACTIVE,
+ RenderParameters.DRAW_ALL_LAYERS,
+ null,
+ Color.RED
+ ),
100,
1234567,
null,
@@ -338,6 +396,15 @@
val interactiveInstance =
interactiveInstanceFuture.get(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)!!
+ interactiveInstance.updateComplicationData(
+ mapOf(
+ EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
+ ShortTextComplicationData.Builder(ComplicationText.plain("Test")).build(),
+ EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID to
+ LongTextComplicationData.Builder(ComplicationText.plain("Test")).build()
+ )
+ )
+
assertThat(interactiveInstance.complicationState.size).isEqualTo(2)
val leftComplicationDetails = interactiveInstance.complicationState[
@@ -359,6 +426,9 @@
ComplicationType.SMALL_IMAGE
)
assertTrue(leftComplicationDetails.isEnabled)
+ assertThat(leftComplicationDetails.currentType).isEqualTo(
+ ComplicationType.SHORT_TEXT
+ )
val rightComplicationDetails = interactiveInstance.complicationState[
EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
@@ -379,6 +449,9 @@
ComplicationType.SMALL_IMAGE
)
assertTrue(rightComplicationDetails.isEnabled)
+ assertThat(rightComplicationDetails.currentType).isEqualTo(
+ ComplicationType.LONG_TEXT
+ )
interactiveInstance.close()
}
@@ -530,7 +603,12 @@
)
val bitmap = interactiveInstance.takeWatchFaceScreenshot(
- RenderParameters(DrawMode.INTERACTIVE, RenderParameters.DRAW_ALL_LAYERS, null),
+ RenderParameters(
+ DrawMode.INTERACTIVE,
+ RenderParameters.DRAW_ALL_LAYERS,
+ null,
+ Color.RED
+ ),
100,
1234567,
null,
@@ -544,6 +622,36 @@
interactiveInstance.close()
}
}
+
+ @Test
+ fun getComplicationIdAt() {
+ val interactiveInstanceFuture =
+ service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient(
+ "testId",
+ deviceConfig,
+ systemState,
+ null,
+ complications
+ )
+
+ Mockito.`when`(surfaceHolder.surfaceFrame)
+ .thenReturn(Rect(0, 0, 400, 400))
+
+ // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
+ createEngine()
+
+ val interactiveInstance =
+ interactiveInstanceFuture.get(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)!!
+
+ assertNull(interactiveInstance.getComplicationIdAt(0, 0))
+ assertThat(interactiveInstance.getComplicationIdAt(85, 165)).isEqualTo(
+ EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
+ )
+ assertThat(interactiveInstance.getComplicationIdAt(255, 165)).isEqualTo(
+ EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
+ )
+ interactiveInstance.close()
+ }
}
internal class TestExampleCanvasAnalogWatchFaceService(
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/ComplicationState.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/ComplicationState.kt
index 847643a..5c2dda8 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/ComplicationState.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/ComplicationState.kt
@@ -18,6 +18,7 @@
import android.graphics.Rect
import androidx.wear.complications.DefaultComplicationProviderPolicy
+import androidx.wear.complications.data.ComplicationData
import androidx.wear.complications.data.ComplicationType
import androidx.wear.watchface.Complication
import androidx.wear.watchface.data.ComplicationBoundsType
@@ -42,7 +43,10 @@
/** Whether or not the complication is drawn. */
@get:JvmName("isEnabled")
- public val isEnabled: Boolean
+ public val isEnabled: Boolean,
+
+ /** The [ComplicationType] of the complication's current [ComplicationData]. */
+ public val currentType: ComplicationType
) {
internal constructor(
complicationStateWireFormat: ComplicationStateWireFormat
@@ -55,6 +59,7 @@
complicationStateWireFormat.fallbackSystemProvider
),
ComplicationType.fromWireType(complicationStateWireFormat.defaultProviderType),
- complicationStateWireFormat.isEnabled
+ complicationStateWireFormat.isEnabled,
+ ComplicationType.fromWireType(complicationStateWireFormat.currentType)
)
}
\ No newline at end of file
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt
index 1205802..65d2a76 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt
@@ -20,11 +20,13 @@
import android.os.IBinder
import android.support.wearable.watchface.SharedMemoryImage
import androidx.annotation.IntRange
+import androidx.annotation.Px
import androidx.annotation.RequiresApi
import androidx.wear.complications.data.ComplicationData
import androidx.wear.watchface.RenderParameters
import androidx.wear.watchface.control.IInteractiveWatchFaceWCS
import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
+import androidx.wear.watchface.data.ComplicationBoundsType
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
import androidx.wear.watchface.style.UserStyle
import androidx.wear.watchface.style.UserStyleSchema
@@ -107,6 +109,24 @@
/** Returns the associated [IBinder]. Allows this interface to be passed over AIDL. */
public fun asBinder(): IBinder
+
+ /** Returns the ID of the complication at the given coordinates or `null` if there isn't one.*/
+ @SuppressWarnings("AutoBoxing")
+ public fun getComplicationIdAt(@Px x: Int, @Px y: Int): Int? =
+ complicationState.asSequence().firstOrNull {
+ it.value.isEnabled && when (it.value.boundsType) {
+ ComplicationBoundsType.ROUND_RECT -> it.value.bounds.contains(x, y)
+ ComplicationBoundsType.BACKGROUND -> false
+ ComplicationBoundsType.EDGE -> false
+ else -> false
+ }
+ }?.key
+
+ /**
+ * Requests the specified complication is highlighted for a short period to bring attention to
+ * it.
+ */
+ public fun bringAttentionToComplication(complicationId: Int)
}
/** Controls a stateful remote interactive watch face with an interface tailored for WCS. */
@@ -177,4 +197,8 @@
}
override fun asBinder(): IBinder = iInteractiveWatchFaceWcs.asBinder()
+
+ override fun bringAttentionToComplication(complicationId: Int) {
+ iInteractiveWatchFaceWcs.bringAttentionToComplication(complicationId)
+ }
}
\ No newline at end of file
diff --git a/wear/wear-watchface-data/api/restricted_current.txt b/wear/wear-watchface-data/api/restricted_current.txt
index f95a226..62ee931 100644
--- a/wear/wear-watchface-data/api/restricted_current.txt
+++ b/wear/wear-watchface-data/api/restricted_current.txt
@@ -188,10 +188,11 @@
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public final class ComplicationStateWireFormat implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
- ctor public ComplicationStateWireFormat(android.graphics.Rect, @androidx.wear.watchface.data.ComplicationBoundsType int, @android.support.wearable.complications.ComplicationData.ComplicationType int[], java.util.List<android.content.ComponentName!>?, int, @android.support.wearable.complications.ComplicationData.ComplicationType int, boolean);
+ ctor public ComplicationStateWireFormat(android.graphics.Rect, @androidx.wear.watchface.data.ComplicationBoundsType int, @android.support.wearable.complications.ComplicationData.ComplicationType int[], java.util.List<android.content.ComponentName!>?, int, @android.support.wearable.complications.ComplicationData.ComplicationType int, boolean, @android.support.wearable.complications.ComplicationData.ComplicationType int);
method public int describeContents();
method public android.graphics.Rect getBounds();
method @androidx.wear.watchface.data.ComplicationBoundsType public int getBoundsType();
+ method @android.support.wearable.complications.ComplicationData.ComplicationType public int getCurrentType();
method @android.support.wearable.complications.ComplicationData.ComplicationType public int getDefaultProviderType();
method public java.util.List<android.content.ComponentName!>? getDefaultProvidersToTry();
method public int getFallbackSystemProvider();
@@ -231,9 +232,10 @@
}
@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, java.util.List<androidx.wear.watchface.data.RenderParametersWireFormat.LayerParameterWireFormat!>, Integer?);
+ ctor public RenderParametersWireFormat(int, java.util.List<androidx.wear.watchface.data.RenderParametersWireFormat.LayerParameterWireFormat!>, Integer?, @ColorInt int);
method public int describeContents();
method public int getDrawMode();
+ method @ColorInt public int getHighlightTint();
method public Integer? getHighlightedComplicationId();
method public java.util.List<androidx.wear.watchface.data.RenderParametersWireFormat.LayerParameterWireFormat!> getLayerParameters();
method public void writeToParcel(android.os.Parcel, int);
diff --git a/wear/wear-watchface-data/build.gradle b/wear/wear-watchface-data/build.gradle
index f32aa2f..97a32fd 100644
--- a/wear/wear-watchface-data/build.gradle
+++ b/wear/wear-watchface-data/build.gradle
@@ -48,6 +48,10 @@
buildFeatures {
aidl = true
}
+
+ buildTypes.all {
+ consumerProguardFiles 'proguard-rules.pro'
+ }
}
androidx {
diff --git a/wear/wear-watchface-data/proguard-rules.pro b/wear/wear-watchface-data/proguard-rules.pro
new file mode 100644
index 0000000..f50b7ac
--- /dev/null
+++ b/wear/wear-watchface-data/proguard-rules.pro
@@ -0,0 +1,16 @@
+# 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.
+
+# Prevent Parcelizer objects from being removed or renamed.
+-keep public class androidx.wear.watchface.**Parcelizer { *; }
\ No newline at end of file
diff --git a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFaceWCS.aidl b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFaceWCS.aidl
index 40eb511..667c27f 100644
--- a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFaceWCS.aidl
+++ b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFaceWCS.aidl
@@ -32,7 +32,7 @@
interface IInteractiveWatchFaceWCS {
// IMPORTANT NOTE: All methods must be given an explicit transaction id that must never change
// in the future to remain binary backwards compatible.
- // Next Id: 12
+ // Next Id: 13
/**
* API version number. This should be incremented every time a new method is added.
@@ -114,4 +114,12 @@
* @since API version 1.
*/
oneway void release() = 11;
+
+ /**
+ * Requests the specified complication is highlighted for a short period to bring attention to
+ * it.
+ *
+ * @since API version 1.
+ */
+ oneway void bringAttentionToComplication(in int complicationId) = 12;
}
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/ComplicationStateWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/ComplicationStateWireFormat.java
index 9d5b032..6fb369ab 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/ComplicationStateWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/ComplicationStateWireFormat.java
@@ -69,6 +69,10 @@
@ParcelField(7)
boolean mIsEnabled;
+ @ParcelField(8)
+ @ComplicationData.ComplicationType
+ int mCurrentType;
+
/** Used by VersionedParcelable. */
ComplicationStateWireFormat() {
}
@@ -80,7 +84,8 @@
@Nullable List<ComponentName> defaultProvidersToTry,
int fallbackSystemProvider,
@ComplicationData.ComplicationType int defaultProviderType,
- boolean isEnabled) {
+ boolean isEnabled,
+ @ComplicationData.ComplicationType int currentType) {
mBounds = bounds;
mBoundsType = boundsType;
mSupportedTypes = supportedTypes;
@@ -88,6 +93,7 @@
mFallbackSystemProvider = fallbackSystemProvider;
mDefaultProviderType = defaultProviderType;
mIsEnabled = isEnabled;
+ mCurrentType = currentType;
}
@NonNull
@@ -132,6 +138,12 @@
return mIsEnabled;
}
+ @NonNull
+ @ComplicationData.ComplicationType
+ public int getCurrentType() {
+ return mCurrentType;
+ }
+
/** Serializes this ComplicationDetails to the specified {@link Parcel}. */
@Override
public void writeToParcel(@NonNull Parcel parcel, int flags) {
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 4fec0b0..01a506d 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
@@ -20,6 +20,7 @@
import android.os.Parcel;
import android.os.Parcelable;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
@@ -39,7 +40,6 @@
@VersionedParcelize
@SuppressLint("BanParcelableUsage") // TODO(b/169214666): Remove Parcelable
public class RenderParametersWireFormat implements VersionedParcelable, Parcelable {
- /** */
private static final int NO_COMPLICATION_ID = -1;
/** Wire format for {@link androidx.wear.watchface.DrawMode}. */
@@ -55,6 +55,12 @@
int mHighlightedComplicationId;
/**
+ * Specifies the tint for any highlight.
+ */
+ @ParcelField(3)
+ int mHighlightTint;
+
+ /**
* Wire format for Map<{@link androidx.wear.watchface.style.Layer},
* {@link androidx.wear.watchface.LayerMode}>.
*
@@ -66,18 +72,19 @@
@ParcelField(100)
List<LayerParameterWireFormat> mLayerParameters;
-
RenderParametersWireFormat() {
}
public RenderParametersWireFormat(
int drawMode,
@NonNull List<LayerParameterWireFormat> layerParameters,
- @Nullable Integer highlightedComplicationId) {
+ @Nullable Integer highlightedComplicationId,
+ @ColorInt int highlightTint) {
mDrawMode = drawMode;
mLayerParameters = layerParameters;
mHighlightedComplicationId = (highlightedComplicationId != null)
? highlightedComplicationId : NO_COMPLICATION_ID;
+ mHighlightTint = highlightTint;
}
public int getDrawMode() {
@@ -90,6 +97,11 @@
mHighlightedComplicationId;
}
+ @ColorInt
+ public int getHighlightTint() {
+ return mHighlightTint;
+ }
+
@NonNull
public List<LayerParameterWireFormat> getLayerParameters() {
return mLayerParameters;
diff --git a/wear/wear-watchface/api/current.txt b/wear/wear-watchface/api/current.txt
index d3c5678..4fb343b 100644
--- a/wear/wear-watchface/api/current.txt
+++ b/wear/wear-watchface/api/current.txt
@@ -15,7 +15,7 @@
public class CanvasComplicationDrawable implements androidx.wear.watchface.CanvasComplication {
ctor public CanvasComplicationDrawable(androidx.wear.watchface.complications.rendering.ComplicationDrawable drawable, androidx.wear.watchface.WatchState watchState);
- method public void drawOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+ method public void drawOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar, @ColorInt int color);
method public final androidx.wear.watchface.complications.rendering.ComplicationDrawable getDrawable();
method public androidx.wear.complications.data.IdAndComplicationData? getIdAndData();
method @UiThread public boolean isHighlighted();
@@ -67,12 +67,12 @@
public final class ComplicationOutlineRenderer {
ctor public ComplicationOutlineRenderer();
- method public static void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds);
+ method public static void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
field public static final androidx.wear.watchface.ComplicationOutlineRenderer.Companion Companion;
}
public static final class ComplicationOutlineRenderer.Companion {
- method public void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds);
+ method public void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
}
public final class ComplicationsManager {
@@ -138,11 +138,13 @@
}
public final class RenderParameters {
- ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Map<androidx.wear.watchface.style.Layer,? extends androidx.wear.watchface.LayerMode> layerParameters, Integer? highlightedComplicationId);
+ ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Map<androidx.wear.watchface.style.Layer,? extends androidx.wear.watchface.LayerMode> layerParameters, Integer? highlightedComplicationId, @ColorInt int highlightTint);
method public androidx.wear.watchface.DrawMode getDrawMode();
+ method public int getHighlightTint();
method public Integer? getHighlightedComplicationId();
method public java.util.Map<androidx.wear.watchface.style.Layer,androidx.wear.watchface.LayerMode> getLayerParameters();
property public final androidx.wear.watchface.DrawMode drawMode;
+ property public final int highlightTint;
property public final Integer? highlightedComplicationId;
property public final java.util.Map<androidx.wear.watchface.style.Layer,androidx.wear.watchface.LayerMode> layerParameters;
field public static final androidx.wear.watchface.RenderParameters.Companion Companion;
diff --git a/wear/wear-watchface/api/public_plus_experimental_current.txt b/wear/wear-watchface/api/public_plus_experimental_current.txt
index d3c5678..4fb343b 100644
--- a/wear/wear-watchface/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface/api/public_plus_experimental_current.txt
@@ -15,7 +15,7 @@
public class CanvasComplicationDrawable implements androidx.wear.watchface.CanvasComplication {
ctor public CanvasComplicationDrawable(androidx.wear.watchface.complications.rendering.ComplicationDrawable drawable, androidx.wear.watchface.WatchState watchState);
- method public void drawOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+ method public void drawOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar, @ColorInt int color);
method public final androidx.wear.watchface.complications.rendering.ComplicationDrawable getDrawable();
method public androidx.wear.complications.data.IdAndComplicationData? getIdAndData();
method @UiThread public boolean isHighlighted();
@@ -67,12 +67,12 @@
public final class ComplicationOutlineRenderer {
ctor public ComplicationOutlineRenderer();
- method public static void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds);
+ method public static void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
field public static final androidx.wear.watchface.ComplicationOutlineRenderer.Companion Companion;
}
public static final class ComplicationOutlineRenderer.Companion {
- method public void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds);
+ method public void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
}
public final class ComplicationsManager {
@@ -138,11 +138,13 @@
}
public final class RenderParameters {
- ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Map<androidx.wear.watchface.style.Layer,? extends androidx.wear.watchface.LayerMode> layerParameters, Integer? highlightedComplicationId);
+ ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Map<androidx.wear.watchface.style.Layer,? extends androidx.wear.watchface.LayerMode> layerParameters, Integer? highlightedComplicationId, @ColorInt int highlightTint);
method public androidx.wear.watchface.DrawMode getDrawMode();
+ method public int getHighlightTint();
method public Integer? getHighlightedComplicationId();
method public java.util.Map<androidx.wear.watchface.style.Layer,androidx.wear.watchface.LayerMode> getLayerParameters();
property public final androidx.wear.watchface.DrawMode drawMode;
+ property public final int highlightTint;
property public final Integer? highlightedComplicationId;
property public final java.util.Map<androidx.wear.watchface.style.Layer,androidx.wear.watchface.LayerMode> layerParameters;
field public static final androidx.wear.watchface.RenderParameters.Companion Companion;
diff --git a/wear/wear-watchface/api/restricted_current.txt b/wear/wear-watchface/api/restricted_current.txt
index 6eddf21..3ea9707 100644
--- a/wear/wear-watchface/api/restricted_current.txt
+++ b/wear/wear-watchface/api/restricted_current.txt
@@ -15,7 +15,7 @@
public class CanvasComplicationDrawable implements androidx.wear.watchface.CanvasComplication {
ctor public CanvasComplicationDrawable(androidx.wear.watchface.complications.rendering.ComplicationDrawable drawable, androidx.wear.watchface.WatchState watchState);
- method public void drawOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+ method public void drawOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar, @ColorInt int color);
method public final androidx.wear.watchface.complications.rendering.ComplicationDrawable getDrawable();
method public androidx.wear.complications.data.IdAndComplicationData? getIdAndData();
method @UiThread public boolean isHighlighted();
@@ -67,12 +67,12 @@
public final class ComplicationOutlineRenderer {
ctor public ComplicationOutlineRenderer();
- method public static void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds);
+ method public static void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
field public static final androidx.wear.watchface.ComplicationOutlineRenderer.Companion Companion;
}
public static final class ComplicationOutlineRenderer.Companion {
- method public void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds);
+ method public void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
}
public final class ComplicationsManager {
@@ -163,13 +163,15 @@
}
public final class RenderParameters {
- ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Map<androidx.wear.watchface.style.Layer,? extends androidx.wear.watchface.LayerMode> layerParameters, Integer? highlightedComplicationId);
+ ctor public RenderParameters(androidx.wear.watchface.DrawMode drawMode, java.util.Map<androidx.wear.watchface.style.Layer,? extends androidx.wear.watchface.LayerMode> layerParameters, Integer? highlightedComplicationId, @ColorInt int highlightTint);
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 int getHighlightTint();
method public Integer? getHighlightedComplicationId();
method public java.util.Map<androidx.wear.watchface.style.Layer,androidx.wear.watchface.LayerMode> getLayerParameters();
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 int highlightTint;
property public final Integer? highlightedComplicationId;
property public final java.util.Map<androidx.wear.watchface.style.Layer,androidx.wear.watchface.LayerMode> layerParameters;
field public static final androidx.wear.watchface.RenderParameters.Companion Companion;
diff --git a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/TestCanvasAnalogWatchFaceService.kt b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/TestCanvasAnalogWatchFaceService.kt
index 98f48df..26d6d79 100644
--- a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/TestCanvasAnalogWatchFaceService.kt
+++ b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/TestCanvasAnalogWatchFaceService.kt
@@ -31,7 +31,7 @@
private val handler: Handler,
var mockSystemTimeMillis: Long,
var surfaceHolderOverride: SurfaceHolder,
- var userUnlocked: Boolean
+ var preRInitFlow: Boolean
) : WatchFaceService() {
private val mutableWatchState = MutableWatchState().apply {
@@ -68,5 +68,5 @@
override fun getWallpaperSurfaceHolderOverride() = surfaceHolderOverride
- override fun isUserUnlocked() = userUnlocked
+ override fun expectPreRInitFlow() = preRInitFlow
}
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 0e28d2f..95a9ebb 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
@@ -19,6 +19,7 @@
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.graphics.Color
import android.support.wearable.watchface.SharedMemoryImage
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -88,7 +89,8 @@
RenderParameters(
DrawMode.INTERACTIVE,
RenderParameters.DRAW_ALL_LAYERS,
- null
+ null,
+ Color.RED
).toWireFormat(),
100,
1234567890,
@@ -128,7 +130,8 @@
RenderParameters(
DrawMode.AMBIENT,
RenderParameters.DRAW_ALL_LAYERS,
- null
+ null,
+ Color.RED
).toWireFormat(),
100,
123456789,
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 2c9375c..6d0daab 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
@@ -19,6 +19,7 @@
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
+import android.graphics.Color
import android.graphics.Rect
import android.graphics.SurfaceTexture
import android.os.Handler
@@ -248,7 +249,8 @@
RenderParameters(
DrawMode.AMBIENT,
RenderParameters.DRAW_ALL_LAYERS,
- null
+ null,
+ Color.RED
).toWireFormat(),
100,
123456789,
@@ -281,7 +283,8 @@
RenderParameters(
DrawMode.INTERACTIVE,
RenderParameters.DRAW_ALL_LAYERS,
- null
+ null,
+ Color.RED
).toWireFormat(),
100,
123456789,
@@ -333,7 +336,8 @@
Layer.COMPLICATIONS to LayerMode.DRAW_HIGHLIGHTED,
Layer.TOP_LAYER to LayerMode.DRAW
),
- null
+ null,
+ Color.RED
).toWireFormat(),
100,
123456789,
@@ -370,7 +374,8 @@
Layer.COMPLICATIONS to LayerMode.DRAW_HIGHLIGHTED,
Layer.TOP_LAYER to LayerMode.DRAW
),
- EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
+ EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID,
+ Color.RED
).toWireFormat(),
100,
123456789,
@@ -418,7 +423,8 @@
RenderParameters(
DrawMode.INTERACTIVE,
RenderParameters.DRAW_ALL_LAYERS,
- null
+ null,
+ Color.RED
).toWireFormat(),
100,
123456789,
@@ -450,9 +456,9 @@
// Simulate device shutting down.
InteractiveInstanceManager.deleteInstance(INTERACTIVE_INSTANCE_ID)
- // Simulate a direct boot scenario where a new service is created with a locked user
- // but there's no pending PendingWallpaperInteractiveWatchFaceInstance and no
- // wallpaper command. This should load the direct boot parameters which get saved.
+ // Simulate a R style direct boot scenario where a new service is created but there's no
+ // pending PendingWallpaperInteractiveWatchFaceInstance and no wallpaper command. This
+ // should load the direct boot parameters which get saved.
val service2 = TestCanvasAnalogWatchFaceService(
ApplicationProvider.getApplicationContext<Context>(),
handler,
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/BroadcastReceivers.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/BroadcastReceivers.kt
new file mode 100644
index 0000000..78f928f
--- /dev/null
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/BroadcastReceivers.kt
@@ -0,0 +1,162 @@
+/*
+ * 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.wear.watchface
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import androidx.annotation.UiThread
+import androidx.annotation.VisibleForTesting
+
+/**
+ * All watchface instances share the same [Context] which is a problem for broadcast receivers
+ * because the OS will mistakenly believe we're leaking them if there's more than one instance. So
+ * we need to use this class to share them.
+ */
+internal class BroadcastReceivers private constructor(private val context: Context) {
+
+ interface BroadcastEventObserver {
+ /** Called when we receive Intent.ACTION_TIME_TICK. */
+ @UiThread
+ fun onActionTimeTick()
+
+ /** Called when we receive Intent.ACTION_TIMEZONE_CHANGED. */
+ @UiThread
+ fun onActionTimeZoneChanged()
+
+ /** Called when we receive Intent.ACTION_TIME_CHANGED. */
+ @UiThread
+ fun onActionTimeChanged()
+
+ /** Called when we receive Intent.ACTION_BATTERY_CHANGED. */
+ @UiThread
+ fun onActionBatteryChanged(intent: Intent)
+
+ /** Called when we receive Intent.MOCK_TIME_INTENT. */
+ @UiThread
+ fun onMockTime(intent: Intent)
+ }
+
+ companion object {
+ val broadcastEventObservers = HashSet<BroadcastEventObserver>()
+
+ /* We don't leak due to balanced calls to[addBroadcastEventObserver] and
+ [removeBroadcastEventObserver] which sets this back to null.
+ */
+ @SuppressWarnings("StaticFieldLeak")
+ var broadcastReceivers: BroadcastReceivers? = null
+
+ @UiThread
+ fun addBroadcastEventObserver(context: Context, observer: BroadcastEventObserver) {
+ broadcastEventObservers.add(observer)
+ if (broadcastReceivers == null) {
+ broadcastReceivers = BroadcastReceivers(context)
+ }
+ }
+
+ @UiThread
+ fun removeBroadcastEventObserver(observer: BroadcastEventObserver) {
+ broadcastEventObservers.remove(observer)
+ if (broadcastEventObservers.isEmpty()) {
+ broadcastReceivers!!.onDestroy()
+ broadcastReceivers = null
+ }
+ }
+
+ @VisibleForTesting
+ fun sendOnActionBatteryChangedForTesting(intent: Intent) {
+ require(broadcastEventObservers.isNotEmpty())
+ for (observer in broadcastEventObservers) {
+ observer.onActionBatteryChanged(intent)
+ }
+ }
+
+ @VisibleForTesting
+ fun sendOnMockTimeForTesting(intent: Intent) {
+ require(broadcastEventObservers.isNotEmpty())
+ for (observer in broadcastEventObservers) {
+ observer.onMockTime(intent)
+ }
+ }
+ }
+
+ private val actionTimeTickReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+ @SuppressWarnings("SyntheticAccessor")
+ override fun onReceive(context: Context, intent: Intent) {
+ for (observer in broadcastEventObservers) {
+ observer.onActionTimeTick()
+ }
+ }
+ }
+
+ private val actionTimeZoneReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ for (observer in broadcastEventObservers) {
+ observer.onActionTimeZoneChanged()
+ }
+ }
+ }
+
+ private val actionTimeReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ for (observer in broadcastEventObservers) {
+ observer.onActionTimeChanged()
+ }
+ }
+ }
+
+ private val actionBatteryLevelReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+ @SuppressWarnings("SyntheticAccessor")
+ override fun onReceive(context: Context, intent: Intent) {
+ for (observer in broadcastEventObservers) {
+ observer.onActionBatteryChanged(intent)
+ }
+ }
+ }
+
+ private val mockTimeReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+ @SuppressWarnings("SyntheticAccessor")
+ override fun onReceive(context: Context, intent: Intent) {
+ for (observer in broadcastEventObservers) {
+ observer.onMockTime(intent)
+ }
+ }
+ }
+
+ init {
+ context.registerReceiver(actionTimeTickReceiver, IntentFilter(Intent.ACTION_TIME_TICK))
+ context.registerReceiver(
+ actionTimeZoneReceiver,
+ IntentFilter(Intent.ACTION_TIMEZONE_CHANGED)
+ )
+ context.registerReceiver(actionTimeReceiver, IntentFilter(Intent.ACTION_TIME_CHANGED))
+ context.registerReceiver(
+ actionBatteryLevelReceiver,
+ IntentFilter(Intent.ACTION_BATTERY_CHANGED)
+ )
+ context.registerReceiver(mockTimeReceiver, IntentFilter(WatchFaceImpl.MOCK_TIME_INTENT))
+ }
+
+ fun onDestroy() {
+ context.unregisterReceiver(actionTimeTickReceiver)
+ context.unregisterReceiver(actionTimeZoneReceiver)
+ context.unregisterReceiver(actionTimeReceiver)
+ context.unregisterReceiver(actionBatteryLevelReceiver)
+ context.unregisterReceiver(mockTimeReceiver)
+ }
+}
\ No newline at end of file
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
index 8454576..eb36e4a 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
@@ -23,6 +23,7 @@
import android.graphics.drawable.Drawable
import android.icu.util.Calendar
import android.support.wearable.complications.ComplicationData
+import androidx.annotation.ColorInt
import androidx.annotation.UiThread
import androidx.wear.complications.ComplicationBounds
import androidx.wear.complications.ComplicationHelperActivity
@@ -163,7 +164,7 @@
if (renderParameters.highlightedComplicationId == null ||
renderParameters.highlightedComplicationId == idAndData?.complicationId
) {
- drawOutline(canvas, bounds, calendar)
+ drawOutline(canvas, bounds, calendar, renderParameters.highlightTint)
}
}
LayerMode.HIDE -> return
@@ -174,9 +175,14 @@
public open fun drawOutline(
canvas: Canvas,
bounds: Rect,
- calendar: Calendar
+ calendar: Calendar,
+ @ColorInt color: Int
) {
- ComplicationOutlineRenderer.drawComplicationSelectOutline(canvas, bounds)
+ ComplicationOutlineRenderer.drawComplicationSelectOutline(
+ canvas,
+ bounds,
+ color
+ )
}
/**
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationOutlineRenderer.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationOutlineRenderer.kt
index 4d5bb5d..907b83e 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationOutlineRenderer.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationOutlineRenderer.kt
@@ -17,9 +17,9 @@
package androidx.wear.watchface
import android.graphics.Canvas
-import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
+import androidx.annotation.ColorInt
import kotlin.math.cos
import kotlin.math.sin
@@ -38,12 +38,16 @@
strokeWidth = DASH_WIDTH
style = Paint.Style.FILL_AND_STROKE
isAntiAlias = true
- color = Color.RED
}
/** Draws a thick dotted line around the complication with the given bounds. */
@JvmStatic
- public fun drawComplicationSelectOutline(canvas: Canvas, bounds: Rect) {
+ public fun drawComplicationSelectOutline(
+ canvas: Canvas,
+ bounds: Rect,
+ @ColorInt color: Int
+ ) {
+ dashPaint.color = color
if (bounds.width() == bounds.height()) {
drawCircleDashBorder(canvas, bounds)
return
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt
index 52ab4b0..91e83df 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt
@@ -21,7 +21,7 @@
/**
* An observable UI thread only data holder class (see [Observer]).
*
- * @param <T> The type of data hold by this instance
+ * @param T The type of data held by this instance
*/
public open class ObservableWatchData<T : Any> internal constructor(internal var _value: T?) {
@@ -100,7 +100,7 @@
/**
* [ObservableWatchData] which publicly exposes [setValue(T)] method.
*
- * @param <T> The type of data hold by this instance
+ * @param T The type of data held by this instance
*/
public class MutableObservableWatchData<T : Any>(initialValue: T?) :
ObservableWatchData<T>(initialValue) {
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 d102e50..46e38ef 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
@@ -16,6 +16,8 @@
package androidx.wear.watchface
+import android.graphics.Color
+import androidx.annotation.ColorInt
import androidx.annotation.RestrictTo
import androidx.wear.watchface.data.RenderParametersWireFormat
import androidx.wear.watchface.style.Layer
@@ -48,7 +50,8 @@
DRAW,
/**
- * This layer should be rendered with highlighting (used by the editor). See also
+ * This layer should be rendered with highlighting (used by the editor) using
+ * [RenderParameters.highlightTint]. See also
* [RenderParameters.highlightedComplicationId] for use in combination with
* [Layer.COMPLICATIONS].
*/
@@ -77,7 +80,11 @@
*/
@SuppressWarnings("AutoBoxing")
@get:SuppressWarnings("AutoBoxing")
- public val highlightedComplicationId: Int?
+ public val highlightedComplicationId: Int?,
+
+ /** Specifies the tint should be used for highlights. */
+ @ColorInt
+ public val highlightTint: Int
) {
public companion object {
/** A layerParameters map where all Layers have [LayerMode.DRAW]. */
@@ -88,7 +95,7 @@
/** Default RenderParameters which draws everything in interactive mode. */
@JvmField
public val DEFAULT_INTERACTIVE: RenderParameters =
- RenderParameters(DrawMode.INTERACTIVE, DRAW_ALL_LAYERS, null)
+ RenderParameters(DrawMode.INTERACTIVE, DRAW_ALL_LAYERS, null, Color.RED)
}
/** @hide */
@@ -99,7 +106,8 @@
{ Layer.values()[it.layer] },
{ LayerMode.values()[it.layerMode] }
),
- wireFormat.highlightedComplicationId
+ wireFormat.highlightedComplicationId,
+ wireFormat.highlightTint
)
/** @hide */
@@ -112,6 +120,7 @@
it.value.ordinal
)
},
- highlightedComplicationId
+ highlightedComplicationId,
+ highlightTint
)
}
\ No newline at end of file
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
index bb4754b..9c62c0d 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
@@ -17,6 +17,7 @@
package androidx.wear.watchface
import android.annotation.SuppressLint
+import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
@@ -135,8 +136,8 @@
/**
* The bounds of the [SurfaceHolder] this Renderer renders into. Depending on the shape of the
* device's screen not all of these pixels may be visible to the user (see
- * [WatchState.screenShape]). Note also that API level 27+ devices draw indicators in the top
- * and bottom 24dp of the screen, avoid rendering anything important there.
+ * [Configuration.isScreenRound]). Note also that API level 27+ devices draw indicators in the
+ * top and bottom 24dp of the screen, avoid rendering anything important there.
*/
public var screenBounds: Rect = surfaceHolder.surfaceFrame
private set
@@ -249,8 +250,9 @@
}
/**
- * Posts a message to schedule a call to [renderInternal] to draw the next frame. Unlike
- * [invalidate], this method is thread-safe and may be called on any thread.
+ * Posts a message to schedule a call to either [CanvasRenderer.render] or [GlesRenderer.render]
+ * to draw the next frame. Unlike [invalidate], this method is thread-safe and may be called
+ * on any thread.
*/
public fun postInvalidate() {
if (this::watchFaceHostApi.isInitialized) {
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 a101a9c..1feaac7 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
@@ -18,11 +18,10 @@
import android.annotation.SuppressLint
import android.app.NotificationManager
-import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.content.IntentFilter
+import android.graphics.Color
import android.graphics.Point
import android.graphics.Rect
import android.icu.util.Calendar
@@ -39,7 +38,6 @@
import androidx.annotation.VisibleForTesting
import androidx.wear.complications.SystemProviders
import androidx.wear.complications.data.ComplicationData
-import androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle
import androidx.wear.watchface.control.IInteractiveWatchFaceSysUI
import androidx.wear.watchface.data.RenderParametersWireFormat
import androidx.wear.watchface.style.UserStyle
@@ -331,35 +329,6 @@
private val pendingPostDoubleTap: CancellableUniqueTask =
CancellableUniqueTask(watchFaceHostApi.getHandler())
- private val timeZoneReceiver: BroadcastReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- calendar.timeZone = TimeZone.getDefault()
- watchFaceHostApi.invalidate()
- }
- }
-
- private val timeReceiver: BroadcastReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- // System time has changed hence next scheduled draw is invalid.
- nextDrawTimeMillis = systemTimeProvider.getSystemTimeMillis()
- watchFaceHostApi.invalidate()
- }
- }
-
- internal val batteryLevelReceiver: BroadcastReceiver = object : BroadcastReceiver() {
- @SuppressWarnings("SyntheticAccessor")
- override fun onReceive(context: Context, intent: Intent) {
- val isBatteryLowAndNotCharging =
- watchState.isBatteryLowAndNotCharging as MutableObservableWatchData
- when (intent.action) {
- Intent.ACTION_BATTERY_LOW -> isBatteryLowAndNotCharging.value = true
- Intent.ACTION_BATTERY_OKAY -> isBatteryLowAndNotCharging.value = false
- Intent.ACTION_POWER_CONNECTED -> isBatteryLowAndNotCharging.value = false
- }
- watchFaceHostApi.invalidate()
- }
- }
-
private val componentName =
ComponentName(
watchFaceHostApi.getContext().packageName,
@@ -376,14 +345,36 @@
legacyWatchFaceStyle.tapEventsAccepted
)
- /**
- * We listen for MOCK_TIME_INTENTs which we interpret as a request to modify time. E.g. speeding
- * up or slowing down time, and providing support for making time loop between two instants.
- * This is intended to help implement animations which may occur infrequently (e.g. hourly).
- */
- internal val mockTimeReceiver: BroadcastReceiver = object : BroadcastReceiver() {
- @SuppressWarnings("SyntheticAccessor")
- override fun onReceive(context: Context, intent: Intent) {
+ private val broadcastEventObserver = object : BroadcastReceivers.BroadcastEventObserver {
+ override fun onActionTimeTick() {
+ if (!watchState.isAmbient.value) {
+ renderer.invalidate()
+ }
+ }
+
+ override fun onActionTimeZoneChanged() {
+ calendar.timeZone = TimeZone.getDefault()
+ renderer.invalidate()
+ }
+
+ override fun onActionTimeChanged() {
+ // System time has changed hence next scheduled draw is invalid.
+ nextDrawTimeMillis = systemTimeProvider.getSystemTimeMillis()
+ renderer.invalidate()
+ }
+
+ override fun onActionBatteryChanged(intent: Intent) {
+ val isBatteryLowAndNotCharging =
+ watchState.isBatteryLowAndNotCharging as MutableObservableWatchData
+ when (intent.action) {
+ Intent.ACTION_BATTERY_LOW -> isBatteryLowAndNotCharging.value = true
+ Intent.ACTION_BATTERY_OKAY -> isBatteryLowAndNotCharging.value = false
+ Intent.ACTION_POWER_CONNECTED -> isBatteryLowAndNotCharging.value = false
+ }
+ renderer.invalidate()
+ }
+
+ override fun onMockTime(intent: Intent) {
mockTime.speed = intent.getFloatExtra(
EXTRA_MOCK_TIME_SPEED_MULTIPLIER,
MOCK_TIME_DEFAULT_SPEED_MULTIPLIER
@@ -561,21 +552,9 @@
return
}
registeredReceivers = true
- watchFaceHostApi.getContext().registerReceiver(
- timeZoneReceiver,
- IntentFilter(Intent.ACTION_TIMEZONE_CHANGED)
- )
- watchFaceHostApi.getContext().registerReceiver(
- timeReceiver,
- IntentFilter(Intent.ACTION_TIME_CHANGED)
- )
- watchFaceHostApi.getContext().registerReceiver(
- batteryLevelReceiver,
- IntentFilter(Intent.ACTION_BATTERY_CHANGED)
- )
- watchFaceHostApi.getContext().registerReceiver(
- mockTimeReceiver,
- IntentFilter(MOCK_TIME_INTENT)
+ BroadcastReceivers.addBroadcastEventObserver(
+ watchFaceHostApi.getContext(),
+ broadcastEventObserver
)
}
@@ -584,10 +563,7 @@
return
}
registeredReceivers = false
- watchFaceHostApi.getContext().unregisterReceiver(timeZoneReceiver)
- watchFaceHostApi.getContext().unregisterReceiver(timeReceiver)
- watchFaceHostApi.getContext().unregisterReceiver(batteryLevelReceiver)
- watchFaceHostApi.getContext().unregisterReceiver(mockTimeReceiver)
+ BroadcastReceivers.removeBroadcastEventObserver(broadcastEventObserver)
}
private fun scheduleDraw() {
@@ -637,7 +613,12 @@
newDrawMode = DrawMode.MUTE
}
renderer.renderParameters =
- RenderParameters(newDrawMode, RenderParameters.DRAW_ALL_LAYERS, null)
+ RenderParameters(
+ newDrawMode,
+ RenderParameters.DRAW_ALL_LAYERS,
+ null,
+ Color.BLACK // Required by the constructor but unused.
+ )
}
/** @hide */
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 5a99ffc..da8e3e7 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -17,23 +17,21 @@
package androidx.wear.watchface
import android.annotation.SuppressLint
-import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.content.IntentFilter
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Rect
import android.icu.util.Calendar
import android.icu.util.TimeZone
+import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.os.RemoteException
import android.os.Trace
-import android.os.UserManager
import android.service.wallpaper.WallpaperService
import android.support.wearable.watchface.Constants
import android.support.wearable.watchface.IWatchFaceService
@@ -237,8 +235,9 @@
// This is open for use by tests.
internal open fun allowWatchFaceToAnimate() = true
+ // Whether or not the pre R style init flow (SET_BINDER wallpaper command) is expected.
// This is open for use by tests.
- internal open fun isUserUnlocked() = getSystemService(UserManager::class.java).isUserUnlocked
+ internal open fun expectPreRInitFlow() = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
/**
* This is open for use by tests, it allows them to inject a custom [SurfaceHolder].
@@ -263,14 +262,6 @@
isVisible.value = this@EngineWrapper.isVisible
}
- private var timeTickRegistered = false
- private val timeTickReceiver: BroadcastReceiver = object : BroadcastReceiver() {
- @SuppressWarnings("SyntheticAccessor")
- override fun onReceive(context: Context, intent: Intent) {
- watchFaceImpl.renderer.invalidate()
- }
- }
-
/**
* Whether or not we allow watchfaces to animate. In some tests or for headless
* rendering (for remote config) we don't want this.
@@ -303,16 +294,6 @@
private val invalidateRunnable = Runnable(this::invalidate)
- private val ambientTimeTickFilter = IntentFilter().apply {
- addAction(Intent.ACTION_DATE_CHANGED)
- addAction(Intent.ACTION_TIME_CHANGED)
- addAction(Intent.ACTION_TIMEZONE_CHANGED)
- }
-
- private val interactiveTimeTickFilter = IntentFilter(ambientTimeTickFilter).apply {
- addAction(Intent.ACTION_TIME_TICK)
- }
-
// TODO(alexclarke): Figure out if we can remove this.
private var pendingBackgroundAction: Bundle? = null
private var pendingProperties: Bundle? = null
@@ -345,7 +326,7 @@
InteractiveInstanceManager.takePendingWallpaperInteractiveWatchFaceInstance()
// In a direct boot scenario attempt to load the previously serialized parameters.
- if (pendingWallpaperInstance == null && !isUserUnlocked()) {
+ if (pendingWallpaperInstance == null && !expectPreRInitFlow()) {
val params = readDirectBootPrefs(_context, DIRECT_BOOT_PREFS)
if (params != null) {
createInteractiveInstance(params).createWCSApi()
@@ -399,7 +380,6 @@
systemState.inAmbientMode != mutableWatchState.isAmbient.value
) {
mutableWatchState.isAmbient.value = systemState.inAmbientMode
- updateTimeTickReceiver()
}
if (firstSetSystemState ||
@@ -458,7 +438,10 @@
it.value.defaultProviderPolicy.providersAsList(),
it.value.defaultProviderPolicy.systemProviderFallback,
it.value.defaultProviderType.asWireComplicationType(),
- it.value.enabled
+ it.value.enabled,
+ it.value.renderer.idAndData?.complicationData?.type
+ ?.asWireComplicationType()
+ ?: ComplicationType.NO_DATA.asWireComplicationType()
)
)
}
@@ -660,11 +643,6 @@
choreographer.removeFrameCallback(frameCallback)
}
- if (timeTickRegistered) {
- timeTickRegistered = false
- unregisterReceiver(timeTickReceiver)
- }
-
if (this::watchFaceImpl.isInitialized) {
watchFaceImpl.onDestroy()
}
@@ -924,41 +902,6 @@
pendingSetWatchFaceStyle = false
}
- /**
- * Registers [timeTickReceiver] if it should be registered and isn't currently, or
- * unregisters it if it shouldn't be registered but currently is. It also applies the right
- * intent filter depending on whether we are in ambient mode or not.
- */
- internal fun updateTimeTickReceiver() {
- // Separate calls are issued to deliver the state of isAmbient and isVisible, so during
- // init we might not yet know the state of both.
- if (!mutableWatchState.isAmbient.hasValue() ||
- !mutableWatchState.isVisible.hasValue()
- ) {
- return
- }
-
- if (timeTickRegistered) {
- unregisterReceiver(timeTickReceiver)
- timeTickRegistered = false
- }
-
- // We only register if we are visible, otherwise it doesn't make sense to waste cycles.
- if (mutableWatchState.isVisible.value) {
- if (mutableWatchState.isAmbient.value) {
- registerReceiver(timeTickReceiver, ambientTimeTickFilter)
- } else {
- registerReceiver(timeTickReceiver, interactiveTimeTickFilter)
- }
- timeTickRegistered = true
-
- // In case we missed a tick while transitioning from ambient to interactive, we
- // want to make sure the watch face doesn't show stale time when in interactive
- // mode.
- watchFaceImpl.renderer.invalidate()
- }
- }
-
override fun onVisibilityChanged(visible: Boolean) {
super.onVisibilityChanged(visible)
@@ -979,7 +922,6 @@
}
mutableWatchState.isVisible.value = visible
- updateTimeTickReceiver()
pendingVisibilityChanged = null
}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
index e9666da..28a81c3 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
@@ -115,4 +115,10 @@
uiThreadHandler.runOnHandler {
engine.watchFaceImpl.userStyleRepository.schema.toWireFormat()
}
+
+ override fun bringAttentionToComplication(id: Int) {
+ uiThreadHandler.runOnHandler {
+ engine.watchFaceImpl.complicationsManager.bringAttentionToComplication(id)
+ }
+ }
}
\ No newline at end of file
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
index cd68cc9..c732a7c 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
@@ -31,9 +31,9 @@
import androidx.wear.watchface.runOnHandler
/**
- * A service for creating and controlling WatchFaceInstances.
+ * A service for creating and controlling watch face instances.
*
- * @hide
+ * @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@RequiresApi(27)
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ui/ComplicationConfigFragment.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ui/ComplicationConfigFragment.kt
index b8b0dec..2065aee 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ui/ComplicationConfigFragment.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ui/ComplicationConfigFragment.kt
@@ -20,6 +20,7 @@
import android.content.Context
import android.content.Intent
import android.graphics.Canvas
+import android.graphics.Color
import android.graphics.Rect
import android.icu.util.Calendar
import android.os.Bundle
@@ -196,7 +197,8 @@
Layer.COMPLICATIONS to LayerMode.DRAW_HIGHLIGHTED,
Layer.TOP_LAYER to LayerMode.DRAW
),
- null
+ null,
+ Color.RED
).toWireFormat()
)
canvas.drawBitmap(bitmap, drawRect, drawRect, null)
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
index 1165fbe9..7b3158c 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
@@ -106,6 +106,10 @@
override fun getHandler() = handler
override fun getMutableWatchState() = watchState
+
+ fun setIsVisible(isVisible: Boolean) {
+ watchState.isVisible.value = isVisible
+ }
}
/**
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 c8cd451..034a2c6 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
@@ -329,6 +329,7 @@
sendImmutableProperties(engineWrapper, hasLowBitAmbient, hasBurnInProtection)
watchFaceImpl = engineWrapper.watchFaceImpl
+ testWatchFaceService.setIsVisible(true)
}
private fun initWallpaperInteractiveWatchFaceInstance(
@@ -378,6 +379,7 @@
// The [SurfaceHolder] must be sent before binding.
engineWrapper.onSurfaceChanged(surfaceHolder, 0, 100, 100)
watchFaceImpl = engineWrapper.watchFaceImpl
+ testWatchFaceService.setIsVisible(true)
}
private fun sendBinder(engine: WatchFaceService.EngineWrapper, apiVersion: Int) {
@@ -518,8 +520,7 @@
watchState.isAmbient.value = false
testWatchFaceService.mockSystemTimeMillis = 1000L
- watchFaceImpl.mockTimeReceiver.onReceive(
- context,
+ BroadcastReceivers.sendOnMockTimeForTesting(
Intent(WatchFaceImpl.MOCK_TIME_INTENT).apply {
putExtra(WatchFaceImpl.EXTRA_MOCK_TIME_SPEED_MULTIPLIER, 2.0f)
putExtra(WatchFaceImpl.EXTRA_MOCK_TIME_WRAPPING_MIN_TIME, -1L)
@@ -546,8 +547,7 @@
watchState.isAmbient.value = false
testWatchFaceService.mockSystemTimeMillis = 1000L
- watchFaceImpl.mockTimeReceiver.onReceive(
- context,
+ BroadcastReceivers.sendOnMockTimeForTesting(
Intent(WatchFaceImpl.MOCK_TIME_INTENT).apply {
putExtra(WatchFaceImpl.EXTRA_MOCK_TIME_SPEED_MULTIPLIER, 2.0f)
putExtra(WatchFaceImpl.EXTRA_MOCK_TIME_WRAPPING_MIN_TIME, 1000L)
@@ -875,19 +875,13 @@
)
// The delay should change when battery is low.
- watchFaceImpl.batteryLevelReceiver.onReceive(
- context,
- Intent(Intent.ACTION_BATTERY_LOW)
- )
+ BroadcastReceivers.sendOnActionBatteryChangedForTesting(Intent(Intent.ACTION_BATTERY_LOW))
assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0)).isEqualTo(
WatchFaceImpl.MAX_LOW_POWER_INTERACTIVE_UPDATE_RATE_MS
)
// And go back to normal when battery is OK.
- watchFaceImpl.batteryLevelReceiver.onReceive(
- context,
- Intent(Intent.ACTION_BATTERY_OKAY)
- )
+ BroadcastReceivers.sendOnActionBatteryChangedForTesting(Intent(Intent.ACTION_BATTERY_OKAY))
assertThat(watchFaceImpl.computeDelayTillNextFrame(0, 0)).isEqualTo(
INTERACTIVE_UPDATE_RATE_MS
)
diff --git a/wear/wear/api/api_lint.ignore b/wear/wear/api/api_lint.ignore
index aedd625..e513a66 100644
--- a/wear/wear/api/api_lint.ignore
+++ b/wear/wear/api/api_lint.ignore
@@ -25,24 +25,6 @@
Public class androidx.wear.widget.SwipeDismissController stripped of unavailable superclass androidx.wear.widget.DismissController
-MissingGetterMatchingBuilder: androidx.wear.ongoingactivity.OngoingActivity.Builder#setAnimatedIcon(android.graphics.drawable.Icon):
- androidx.wear.ongoingactivity.OngoingActivity does not declare a `getAnimatedIcon()` method matching method androidx.wear.ongoingactivity.OngoingActivity.Builder.setAnimatedIcon(android.graphics.drawable.Icon)
-MissingGetterMatchingBuilder: androidx.wear.ongoingactivity.OngoingActivity.Builder#setAnimatedIcon(int):
- androidx.wear.ongoingactivity.OngoingActivity does not declare a `getAnimatedIcon()` method matching method androidx.wear.ongoingactivity.OngoingActivity.Builder.setAnimatedIcon(int)
-MissingGetterMatchingBuilder: androidx.wear.ongoingactivity.OngoingActivity.Builder#setStaticIcon(android.graphics.drawable.Icon):
- androidx.wear.ongoingactivity.OngoingActivity does not declare a `getStaticIcon()` method matching method androidx.wear.ongoingactivity.OngoingActivity.Builder.setStaticIcon(android.graphics.drawable.Icon)
-MissingGetterMatchingBuilder: androidx.wear.ongoingactivity.OngoingActivity.Builder#setStaticIcon(int):
- androidx.wear.ongoingactivity.OngoingActivity does not declare a `getStaticIcon()` method matching method androidx.wear.ongoingactivity.OngoingActivity.Builder.setStaticIcon(int)
-MissingGetterMatchingBuilder: androidx.wear.ongoingactivity.OngoingActivity.Builder#setStatus(androidx.wear.ongoingactivity.OngoingActivityStatus):
- androidx.wear.ongoingactivity.OngoingActivity does not declare a `getStatus()` method matching method androidx.wear.ongoingactivity.OngoingActivity.Builder.setStatus(androidx.wear.ongoingactivity.OngoingActivityStatus)
-MissingGetterMatchingBuilder: androidx.wear.ongoingactivity.OngoingActivity.Builder#setTouchIntent(android.app.PendingIntent):
- androidx.wear.ongoingactivity.OngoingActivity does not declare a `getTouchIntent()` method matching method androidx.wear.ongoingactivity.OngoingActivity.Builder.setTouchIntent(android.app.PendingIntent)
-MissingGetterMatchingBuilder: androidx.wear.ongoingactivity.OngoingActivity.Builder#setLocusId(androidx.core.content.LocusIdCompat):
- androidx.wear.ongoingactivity.OngoingActivity does not declare a `getLocusId()` method matching method androidx.wear.ongoingactivity.OngoingActivity.Builder.setLocusId(androidx.core.content.LocusIdCompat)
-MissingGetterMatchingBuilder: androidx.wear.ongoingactivity.OngoingActivity.Builder#setOngoingActivityId(int):
- androidx.wear.ongoingactivity.OngoingActivity does not declare a `getOngoingActivityId()` method matching method androidx.wear.ongoingactivity.OngoingActivity.Builder.setOngoingActivityId(int)
-
-
MissingNullability: androidx.wear.activity.ConfirmationActivity#onCreate(android.os.Bundle) parameter #0:
Missing nullability on parameter `savedInstanceState` in method `onCreate`
MissingNullability: androidx.wear.ambient.AmbientMode#attachAmbientSupport(T):
diff --git a/wear/wear/api/current.txt b/wear/wear/api/current.txt
index c6f4bf9..363b0a2 100644
--- a/wear/wear/api/current.txt
+++ b/wear/wear/api/current.txt
@@ -73,6 +73,7 @@
public final class AmbientModeSupport.AmbientController {
method public boolean isAmbient();
method public void setAmbientOffloadEnabled(boolean);
+ method public void setAutoResumeEnabled(boolean);
}
}
@@ -92,6 +93,7 @@
method public androidx.wear.ongoingactivity.OngoingActivity build();
method public androidx.wear.ongoingactivity.OngoingActivity.Builder setAnimatedIcon(android.graphics.drawable.Icon);
method public androidx.wear.ongoingactivity.OngoingActivity.Builder setAnimatedIcon(@DrawableRes int);
+ method public androidx.wear.ongoingactivity.OngoingActivity.Builder setCategory(String);
method public androidx.wear.ongoingactivity.OngoingActivity.Builder setLocusId(androidx.core.content.LocusIdCompat);
method public androidx.wear.ongoingactivity.OngoingActivity.Builder setOngoingActivityId(int);
method public androidx.wear.ongoingactivity.OngoingActivity.Builder setStaticIcon(android.graphics.drawable.Icon);
@@ -103,24 +105,23 @@
public class OngoingActivityData implements androidx.versionedparcelable.VersionedParcelable {
method public static androidx.wear.ongoingactivity.OngoingActivityData? create(android.app.Notification);
method public android.graphics.drawable.Icon? getAnimatedIcon();
+ method public String? getCategory();
method public androidx.core.content.LocusIdCompat? getLocusId();
method public int getOngoingActivityId();
method public android.graphics.drawable.Icon getStaticIcon();
method public androidx.wear.ongoingactivity.OngoingActivityStatus? getStatus();
+ method public long getTimestamp();
method public android.app.PendingIntent getTouchIntent();
method public static boolean hasOngoingActivity(android.app.Notification);
}
- public abstract class OngoingActivityStatus implements androidx.versionedparcelable.VersionedParcelable {
- ctor public OngoingActivityStatus();
- method public abstract long getNextChangeTimeMillis(long);
- method public abstract CharSequence getText(android.content.Context, long);
+ public class OngoingActivityStatus implements androidx.versionedparcelable.VersionedParcelable {
+ method public long getNextChangeTimeMillis(long);
+ method public CharSequence getText(android.content.Context, long);
}
public class TextOngoingActivityStatus extends androidx.wear.ongoingactivity.OngoingActivityStatus {
ctor public TextOngoingActivityStatus(String);
- method public long getNextChangeTimeMillis(long);
- method public CharSequence getText(android.content.Context, long);
}
public class TimerOngoingActivityStatus extends androidx.wear.ongoingactivity.OngoingActivityStatus {
@@ -128,9 +129,7 @@
ctor public TimerOngoingActivityStatus(long, boolean, long);
ctor public TimerOngoingActivityStatus(long, boolean);
ctor public TimerOngoingActivityStatus(long);
- 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();
diff --git a/wear/wear/api/public_plus_experimental_current.txt b/wear/wear/api/public_plus_experimental_current.txt
index ebd25b4..22878f3 100644
--- a/wear/wear/api/public_plus_experimental_current.txt
+++ b/wear/wear/api/public_plus_experimental_current.txt
@@ -73,6 +73,7 @@
public final class AmbientModeSupport.AmbientController {
method public boolean isAmbient();
method public void setAmbientOffloadEnabled(boolean);
+ method public void setAutoResumeEnabled(boolean);
}
}
@@ -92,6 +93,7 @@
method public androidx.wear.ongoingactivity.OngoingActivity build();
method public androidx.wear.ongoingactivity.OngoingActivity.Builder setAnimatedIcon(android.graphics.drawable.Icon);
method public androidx.wear.ongoingactivity.OngoingActivity.Builder setAnimatedIcon(@DrawableRes int);
+ method public androidx.wear.ongoingactivity.OngoingActivity.Builder setCategory(String);
method public androidx.wear.ongoingactivity.OngoingActivity.Builder setLocusId(androidx.core.content.LocusIdCompat);
method public androidx.wear.ongoingactivity.OngoingActivity.Builder setOngoingActivityId(int);
method public androidx.wear.ongoingactivity.OngoingActivity.Builder setStaticIcon(android.graphics.drawable.Icon);
@@ -103,24 +105,23 @@
@androidx.versionedparcelable.VersionedParcelize public class OngoingActivityData implements androidx.versionedparcelable.VersionedParcelable {
method public static androidx.wear.ongoingactivity.OngoingActivityData? create(android.app.Notification);
method public android.graphics.drawable.Icon? getAnimatedIcon();
+ method public String? getCategory();
method public androidx.core.content.LocusIdCompat? getLocusId();
method public int getOngoingActivityId();
method public android.graphics.drawable.Icon getStaticIcon();
method public androidx.wear.ongoingactivity.OngoingActivityStatus? getStatus();
+ method public long getTimestamp();
method public android.app.PendingIntent getTouchIntent();
method public static boolean hasOngoingActivity(android.app.Notification);
}
- @androidx.versionedparcelable.VersionedParcelize public abstract class OngoingActivityStatus implements androidx.versionedparcelable.VersionedParcelable {
- ctor public OngoingActivityStatus();
- method public abstract long getNextChangeTimeMillis(long);
- method public abstract CharSequence getText(android.content.Context, long);
+ @androidx.versionedparcelable.VersionedParcelize public class OngoingActivityStatus implements androidx.versionedparcelable.VersionedParcelable {
+ method public long getNextChangeTimeMillis(long);
+ method public CharSequence getText(android.content.Context, long);
}
@androidx.versionedparcelable.VersionedParcelize public class TextOngoingActivityStatus extends androidx.wear.ongoingactivity.OngoingActivityStatus {
ctor public TextOngoingActivityStatus(String);
- method public long getNextChangeTimeMillis(long);
- method public CharSequence getText(android.content.Context, long);
}
@androidx.versionedparcelable.VersionedParcelize public class TimerOngoingActivityStatus extends androidx.wear.ongoingactivity.OngoingActivityStatus {
@@ -128,9 +129,7 @@
ctor public TimerOngoingActivityStatus(long, boolean, long);
ctor public TimerOngoingActivityStatus(long, boolean);
ctor public TimerOngoingActivityStatus(long);
- 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();
diff --git a/wear/wear/api/restricted_current.txt b/wear/wear/api/restricted_current.txt
index 5640c27..9be0044 100644
--- a/wear/wear/api/restricted_current.txt
+++ b/wear/wear/api/restricted_current.txt
@@ -73,6 +73,7 @@
public final class AmbientModeSupport.AmbientController {
method public boolean isAmbient();
method public void setAmbientOffloadEnabled(boolean);
+ method public void setAutoResumeEnabled(boolean);
}
}
@@ -92,6 +93,7 @@
method public androidx.wear.ongoingactivity.OngoingActivity build();
method public androidx.wear.ongoingactivity.OngoingActivity.Builder setAnimatedIcon(android.graphics.drawable.Icon);
method public androidx.wear.ongoingactivity.OngoingActivity.Builder setAnimatedIcon(@DrawableRes int);
+ method public androidx.wear.ongoingactivity.OngoingActivity.Builder setCategory(String);
method public androidx.wear.ongoingactivity.OngoingActivity.Builder setLocusId(androidx.core.content.LocusIdCompat);
method public androidx.wear.ongoingactivity.OngoingActivity.Builder setOngoingActivityId(int);
method public androidx.wear.ongoingactivity.OngoingActivity.Builder setStaticIcon(android.graphics.drawable.Icon);
@@ -103,24 +105,23 @@
@androidx.versionedparcelable.VersionedParcelize public class OngoingActivityData implements androidx.versionedparcelable.VersionedParcelable {
method public static androidx.wear.ongoingactivity.OngoingActivityData? create(android.app.Notification);
method public android.graphics.drawable.Icon? getAnimatedIcon();
+ method public String? getCategory();
method public androidx.core.content.LocusIdCompat? getLocusId();
method public int getOngoingActivityId();
method public android.graphics.drawable.Icon getStaticIcon();
method public androidx.wear.ongoingactivity.OngoingActivityStatus? getStatus();
+ method public long getTimestamp();
method public android.app.PendingIntent getTouchIntent();
method public static boolean hasOngoingActivity(android.app.Notification);
}
- @androidx.versionedparcelable.VersionedParcelize public abstract class OngoingActivityStatus implements androidx.versionedparcelable.VersionedParcelable {
- ctor public OngoingActivityStatus();
- method public abstract long getNextChangeTimeMillis(long);
- method public abstract CharSequence getText(android.content.Context, long);
+ @androidx.versionedparcelable.VersionedParcelize public class OngoingActivityStatus implements androidx.versionedparcelable.VersionedParcelable {
+ method public long getNextChangeTimeMillis(long);
+ method public CharSequence getText(android.content.Context, long);
}
@androidx.versionedparcelable.VersionedParcelize public class TextOngoingActivityStatus extends androidx.wear.ongoingactivity.OngoingActivityStatus {
ctor public TextOngoingActivityStatus(String);
- method public long getNextChangeTimeMillis(long);
- method public CharSequence getText(android.content.Context, long);
}
@androidx.versionedparcelable.VersionedParcelize public class TimerOngoingActivityStatus extends androidx.wear.ongoingactivity.OngoingActivityStatus {
@@ -128,9 +129,7 @@
ctor public TimerOngoingActivityStatus(long, boolean, long);
ctor public TimerOngoingActivityStatus(long, boolean);
ctor public TimerOngoingActivityStatus(long);
- 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();
diff --git a/wear/wear/build.gradle b/wear/wear/build.gradle
index eae997b..09fdd9a 100644
--- a/wear/wear/build.gradle
+++ b/wear/wear/build.gradle
@@ -34,6 +34,8 @@
implementation "androidx.core:core-ktx:1.5.0-alpha04"
+ annotationProcessor(project(":versionedparcelable:versionedparcelable-compiler"))
+
compileOnly fileTree(dir: '../wear_stubs', include: ['com.google.android.wearable-stubs.jar'])
}
diff --git a/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportResumeTest.java b/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportResumeTest.java
index 2acbbcd..fe6c193 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportResumeTest.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportResumeTest.java
@@ -19,9 +19,9 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
-import androidx.test.rule.ActivityTestRule;
import androidx.wear.widget.util.WakeLockRule;
import com.google.android.wearable.compat.WearableActivityController;
@@ -37,12 +37,27 @@
public final WakeLockRule mWakeLock = new WakeLockRule();
@Rule
- public final ActivityTestRule<AmbientModeSupportResumeTestActivity> mActivityRule =
- new ActivityTestRule<>(AmbientModeSupportResumeTestActivity.class);
+ public final ActivityScenarioRule<AmbientModeSupportResumeTestActivity> mActivityRule =
+ new ActivityScenarioRule<>(AmbientModeSupportResumeTestActivity.class);
@Test
- public void testActivityDefaults() throws Throwable {
+ public void testActivityDefaults() {
assertTrue(WearableActivityController.getLastInstance().isAutoResumeEnabled());
assertFalse(WearableActivityController.getLastInstance().isAmbientEnabled());
}
+
+ @Test
+ public void testActivityAutoResume() {
+ assertTrue(WearableActivityController.getLastInstance().isAutoResumeEnabled());
+
+ // Test disable/enable auto resume with ambient mode disabled
+ assertFalse(WearableActivityController.getLastInstance().isAmbientEnabled());
+ mActivityRule.getScenario().onActivity(activity-> {
+ activity.getAmbientController().setAutoResumeEnabled(false);
+ assertFalse(WearableActivityController.getLastInstance().isAutoResumeEnabled());
+
+ activity.getAmbientController().setAutoResumeEnabled(true);
+ assertTrue(WearableActivityController.getLastInstance().isAutoResumeEnabled());
+ });
+ }
}
diff --git a/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportResumeTestActivity.java b/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportResumeTestActivity.java
index 9571ae7..b7b49c9 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportResumeTestActivity.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportResumeTestActivity.java
@@ -27,4 +27,8 @@
super.onCreate(savedInstanceState);
mAmbientController = AmbientModeSupport.attach(this);
}
+
+ public AmbientModeSupport.AmbientController getAmbientController() {
+ return mAmbientController;
+ }
}
diff --git a/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportTest.java b/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportTest.java
index 271412e..4b15b7a 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportTest.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/ambient/AmbientModeSupportTest.java
@@ -19,9 +19,9 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.rule.ActivityTestRule;
import androidx.wear.widget.util.WakeLockRule;
import com.google.android.wearable.compat.WearableActivityController;
@@ -37,51 +37,51 @@
public final WakeLockRule mWakeLock = new WakeLockRule();
@Rule
- public final ActivityTestRule<AmbientModeSupportTestActivity> mActivityRule =
- new ActivityTestRule<>(AmbientModeSupportTestActivity.class);
+ public final ActivityScenarioRule<AmbientModeSupportTestActivity> mActivityRule =
+ new ActivityScenarioRule<>(AmbientModeSupportTestActivity.class);
@Test
- public void testEnterAmbientCallback() throws Throwable {
- AmbientModeSupportTestActivity activity = mActivityRule.getActivity();
-
- WearableActivityController.getLastInstance().enterAmbient();
- assertTrue(activity.mEnterAmbientCalled);
- assertFalse(activity.mUpdateAmbientCalled);
- assertFalse(activity.mExitAmbientCalled);
- assertFalse(activity.mAmbientOffloadInvalidatedCalled);
+ public void testEnterAmbientCallback() {
+ mActivityRule.getScenario().onActivity(activity-> {
+ WearableActivityController.getLastInstance().enterAmbient();
+ assertTrue(activity.mEnterAmbientCalled);
+ assertFalse(activity.mUpdateAmbientCalled);
+ assertFalse(activity.mExitAmbientCalled);
+ assertFalse(activity.mAmbientOffloadInvalidatedCalled);
+ });
}
@Test
- public void testUpdateAmbientCallback() throws Throwable {
- AmbientModeSupportTestActivity activity = mActivityRule.getActivity();
-
- WearableActivityController.getLastInstance().updateAmbient();
- assertFalse(activity.mEnterAmbientCalled);
- assertTrue(activity.mUpdateAmbientCalled);
- assertFalse(activity.mExitAmbientCalled);
- assertFalse(activity.mAmbientOffloadInvalidatedCalled);
+ public void testUpdateAmbientCallback() {
+ mActivityRule.getScenario().onActivity(activity-> {
+ WearableActivityController.getLastInstance().updateAmbient();
+ assertFalse(activity.mEnterAmbientCalled);
+ assertTrue(activity.mUpdateAmbientCalled);
+ assertFalse(activity.mExitAmbientCalled);
+ assertFalse(activity.mAmbientOffloadInvalidatedCalled);
+ });
}
@Test
- public void testExitAmbientCallback() throws Throwable {
- AmbientModeSupportTestActivity activity = mActivityRule.getActivity();
-
- WearableActivityController.getLastInstance().exitAmbient();
- assertFalse(activity.mEnterAmbientCalled);
- assertFalse(activity.mUpdateAmbientCalled);
- assertTrue(activity.mExitAmbientCalled);
- assertFalse(activity.mAmbientOffloadInvalidatedCalled);
+ public void testExitAmbientCallback() {
+ mActivityRule.getScenario().onActivity(activity-> {
+ WearableActivityController.getLastInstance().exitAmbient();
+ assertFalse(activity.mEnterAmbientCalled);
+ assertFalse(activity.mUpdateAmbientCalled);
+ assertTrue(activity.mExitAmbientCalled);
+ assertFalse(activity.mAmbientOffloadInvalidatedCalled);
+ });
}
@Test
- public void testAmbientOffloadInvalidatedCallback() throws Throwable {
- AmbientModeSupportTestActivity activity = mActivityRule.getActivity();
-
- WearableActivityController.getLastInstance().invalidateAmbientOffload();
- assertFalse(activity.mEnterAmbientCalled);
- assertFalse(activity.mUpdateAmbientCalled);
- assertFalse(activity.mExitAmbientCalled);
- assertTrue(activity.mAmbientOffloadInvalidatedCalled);
+ public void testAmbientOffloadInvalidatedCallback() {
+ mActivityRule.getScenario().onActivity(activity-> {
+ WearableActivityController.getLastInstance().invalidateAmbientOffload();
+ assertFalse(activity.mEnterAmbientCalled);
+ assertFalse(activity.mUpdateAmbientCalled);
+ assertFalse(activity.mExitAmbientCalled);
+ assertTrue(activity.mAmbientOffloadInvalidatedCalled);
+ });
}
@Test
@@ -91,23 +91,38 @@
@Test
public void testCallsControllerIsAmbient() {
- AmbientModeSupportTestActivity activity = mActivityRule.getActivity();
+ mActivityRule.getScenario().onActivity(activity-> {
+ WearableActivityController.getLastInstance().setAmbient(true);
+ assertTrue(activity.getAmbientController().isAmbient());
- WearableActivityController.getLastInstance().setAmbient(true);
- assertTrue(activity.getAmbientController().isAmbient());
-
- WearableActivityController.getLastInstance().setAmbient(false);
- assertFalse(activity.getAmbientController().isAmbient());
+ WearableActivityController.getLastInstance().setAmbient(false);
+ assertFalse(activity.getAmbientController().isAmbient());
+ });
}
@Test
public void testEnableAmbientOffload() {
- AmbientModeSupportTestActivity activity = mActivityRule.getActivity();
+ mActivityRule.getScenario().onActivity(activity-> {
+ activity.getAmbientController().setAmbientOffloadEnabled(true);
+ assertTrue(WearableActivityController.getLastInstance().isAmbientOffloadEnabled());
- activity.getAmbientController().setAmbientOffloadEnabled(true);
- assertTrue(WearableActivityController.getLastInstance().isAmbientOffloadEnabled());
+ activity.getAmbientController().setAmbientOffloadEnabled(false);
+ assertFalse(WearableActivityController.getLastInstance().isAmbientOffloadEnabled());
+ });
+ }
- activity.getAmbientController().setAmbientOffloadEnabled(false);
- assertFalse(WearableActivityController.getLastInstance().isAmbientOffloadEnabled());
+ @Test
+ public void testActivityEnableAutoResume() throws Throwable {
+ assertTrue(WearableActivityController.getLastInstance().isAutoResumeEnabled());
+
+ // Test disable/enable auto resume with ambient mode enabled
+ assertTrue(WearableActivityController.getLastInstance().isAmbientEnabled());
+ mActivityRule.getScenario().onActivity(activity-> {
+ activity.getAmbientController().setAutoResumeEnabled(false);
+ assertFalse(WearableActivityController.getLastInstance().isAutoResumeEnabled());
+
+ activity.getAmbientController().setAutoResumeEnabled(true);
+ assertTrue(WearableActivityController.getLastInstance().isAutoResumeEnabled());
+ });
}
}
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/WearableRecyclerViewTest.java b/wear/wear/src/androidTest/java/androidx/wear/widget/WearableRecyclerViewTest.java
index 3158fbd..840bb02 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/WearableRecyclerViewTest.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/WearableRecyclerViewTest.java
@@ -28,7 +28,6 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
-import android.app.Activity;
import android.content.res.Configuration;
import android.view.View;
@@ -39,10 +38,10 @@
import androidx.test.espresso.action.GeneralSwipeAction;
import androidx.test.espresso.action.Press;
import androidx.test.espresso.action.Swipe;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
import androidx.wear.test.R;
import androidx.wear.widget.util.WakeLockRule;
@@ -65,8 +64,8 @@
public final WakeLockRule wakeLock = new WakeLockRule();
@Rule
- public final ActivityTestRule<WearableRecyclerViewTestActivity> mActivityRule =
- new ActivityTestRule<>(WearableRecyclerViewTestActivity.class, true, true);
+ public final ActivityScenarioRule<WearableRecyclerViewTestActivity> mActivityRule =
+ new ActivityScenarioRule<>(WearableRecyclerViewTestActivity.class);
@Before
public void setUp() {
@@ -75,100 +74,117 @@
@Test
public void testCaseInitState() {
- WearableRecyclerView wrv = new WearableRecyclerView(mActivityRule.getActivity());
- wrv.setLayoutManager(new WearableLinearLayoutManager(wrv.getContext()));
+ mActivityRule.getScenario().onActivity(activity -> {
+ WearableRecyclerView wrv = new WearableRecyclerView(activity);
+ wrv.setLayoutManager(new WearableLinearLayoutManager(wrv.getContext()));
- assertFalse(wrv.isEdgeItemsCenteringEnabled());
- assertFalse(wrv.isCircularScrollingGestureEnabled());
- assertEquals(1.0f, wrv.getBezelFraction(), 0.01f);
- assertEquals(180.0f, wrv.getScrollDegreesPerScreen(), 0.01f);
+ assertFalse(wrv.isEdgeItemsCenteringEnabled());
+ assertFalse(wrv.isCircularScrollingGestureEnabled());
+ assertEquals(1.0f, wrv.getBezelFraction(), 0.01f);
+ assertEquals(180.0f, wrv.getScrollDegreesPerScreen(), 0.01f);
+ });
}
@Test
public void testEdgeItemsCenteringOnAndOff() throws Throwable {
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- WearableRecyclerView wrv =
- (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
- wrv.setEdgeItemsCenteringEnabled(true);
- }
- });
- InstrumentationRegistry.getInstrumentation().waitForIdleSync();
-
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- WearableRecyclerView wrv =
- (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
- View child = wrv.getChildAt(0);
- assertNotNull("child", child);
- Activity activity = mActivityRule.getActivity();
- Configuration configuration = activity.getResources().getConfiguration();
- if (configuration.isScreenRound()) {
- assertEquals((wrv.getHeight() - child.getHeight()) / 2, child.getTop());
- } else {
- assertEquals(0, child.getTop());
+ mActivityRule.getScenario().onActivity(activity -> {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WearableRecyclerView wrv =
+ (WearableRecyclerView) activity.findViewById(R.id.wrv);
+ wrv.setEdgeItemsCenteringEnabled(true);
}
- }
+ });
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ mActivityRule.getScenario().onActivity(activity -> {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WearableRecyclerView wrv =
+ (WearableRecyclerView) activity.findViewById(
+ R.id.wrv);
+ View child = wrv.getChildAt(0);
+ assertNotNull("child", child);
+ Configuration configuration = activity.getResources().getConfiguration();
+ if (configuration.isScreenRound()) {
+ assertEquals((wrv.getHeight() - child.getHeight()) / 2, child.getTop());
+ } else {
+ assertEquals(0, child.getTop());
+ }
+ }
+ });
});
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- WearableRecyclerView wrv =
- (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
- wrv.setEdgeItemsCenteringEnabled(false);
- }
+ mActivityRule.getScenario().onActivity(activity -> {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WearableRecyclerView wrv =
+ (WearableRecyclerView) activity.findViewById(
+ R.id.wrv);
+ wrv.setEdgeItemsCenteringEnabled(false);
+ }
+ });
});
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- WearableRecyclerView wrv =
- (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
- View child = wrv.getChildAt(0);
- assertNotNull("child", child);
- assertEquals(0, child.getTop());
+ mActivityRule.getScenario().onActivity(activity -> {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WearableRecyclerView wrv =
+ (WearableRecyclerView) activity.findViewById(
+ R.id.wrv);
+ View child = wrv.getChildAt(0);
+ assertNotNull("child", child);
+ assertEquals(0, child.getTop());
- }
+ }
+ });
});
}
@Test
public void testEdgeItemsCenteringBeforeChildrenDrawn() throws Throwable {
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- Activity activity = mActivityRule.getActivity();
- WearableRecyclerView wrv = (WearableRecyclerView) activity.findViewById(R.id.wrv);
- RecyclerView.Adapter<WearableRecyclerView.ViewHolder> adapter = wrv.getAdapter();
- wrv.setAdapter(null);
- wrv.setEdgeItemsCenteringEnabled(true);
- wrv.setAdapter(adapter);
- }
+ mActivityRule.getScenario().onActivity(activity -> {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WearableRecyclerView wrv = (WearableRecyclerView) activity.findViewById(
+ R.id.wrv);
+ RecyclerView.Adapter<WearableRecyclerView.ViewHolder> adapter =
+ wrv.getAdapter();
+ wrv.setAdapter(null);
+ wrv.setEdgeItemsCenteringEnabled(true);
+ wrv.setAdapter(adapter);
+ }
+ });
});
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- WearableRecyclerView wrv =
- (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
- // Verify the first child
- View child = wrv.getChildAt(0);
- assertNotNull("child", child);
- Activity activity = mActivityRule.getActivity();
- Configuration configuration = activity.getResources().getConfiguration();
- if (configuration.isScreenRound()) {
- assertEquals((wrv.getHeight() - child.getHeight()) / 2, child.getTop());
- } else {
- assertEquals(0, child.getTop());
+ mActivityRule.getScenario().onActivity(activity -> {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WearableRecyclerView wrv =
+ (WearableRecyclerView) activity.findViewById(
+ R.id.wrv);
+ // Verify the first child
+ View child = wrv.getChildAt(0);
+ assertNotNull("child", child);
+ Configuration configuration = activity.getResources().getConfiguration();
+ if (configuration.isScreenRound()) {
+ assertEquals((wrv.getHeight() - child.getHeight()) / 2, child.getTop());
+ } else {
+ assertEquals(0, child.getTop());
+ }
}
- }
+ });
});
}
@@ -176,20 +192,21 @@
public void testCircularScrollingGesture() throws Throwable {
onView(withId(R.id.wrv)).perform(swipeDownFromTopRight());
assertNotScrolledY(R.id.wrv);
- final WearableRecyclerView wrv =
- (WearableRecyclerView) mActivityRule.getActivity().findViewById(
- R.id.wrv);
- assertFalse(wrv.isCircularScrollingGestureEnabled());
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- WearableRecyclerView wrv = (WearableRecyclerView)
- mActivityRule.getActivity().findViewById(R.id.wrv);
- wrv.setCircularScrollingGestureEnabled(true);
- }
+ mActivityRule.getScenario().onActivity(activity -> {
+ final WearableRecyclerView wrv =
+ (WearableRecyclerView) activity.findViewById(
+ R.id.wrv);
+ assertFalse(wrv.isCircularScrollingGestureEnabled());
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WearableRecyclerView wrv = (WearableRecyclerView)
+ activity.findViewById(R.id.wrv);
+ wrv.setCircularScrollingGestureEnabled(true);
+ }
+ });
+ assertTrue(wrv.isCircularScrollingGestureEnabled());
});
- assertTrue(wrv.isCircularScrollingGestureEnabled());
-
// Explicitly set the swipe to SLOW here to avoid problems with test failures on phone AVDs
// with "Gesture navigation" enabled. This is not a particularly satisfactory fix to this
// problem and ideally we should look to move these tests to use a watch AVD which should
@@ -200,36 +217,39 @@
@Test
public void testCurvedOffsettingHelper() throws Throwable {
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- WearableRecyclerView wrv =
- (WearableRecyclerView) mActivityRule.getActivity().findViewById(R.id.wrv);
- wrv.setLayoutManager(new WearableLinearLayoutManager(wrv.getContext()));
- }
+ mActivityRule.getScenario().onActivity(activity -> {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WearableRecyclerView wrv =
+ (WearableRecyclerView) activity.findViewById(
+ R.id.wrv);
+ wrv.setLayoutManager(new WearableLinearLayoutManager(wrv.getContext()));
+ }
+ });
});
-
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
onView(withId(R.id.wrv)).perform(swipeDownFromTopRight());
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- Activity activity = mActivityRule.getActivity();
- WearableRecyclerView wrv = (WearableRecyclerView) activity.findViewById(R.id.wrv);
- if (activity.getResources().getConfiguration().isScreenRound()) {
- View child = wrv.getChildAt(0);
- assertTrue(child.getLeft() > 0);
- } else {
- for (int i = 0; i < wrv.getChildCount(); i++) {
- assertEquals(0, wrv.getChildAt(i).getLeft());
+ mActivityRule.getScenario().onActivity(activity -> {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WearableRecyclerView wrv = (WearableRecyclerView) activity.findViewById(
+ R.id.wrv);
+ if (activity.getResources().getConfiguration().isScreenRound()) {
+ View child = wrv.getChildAt(0);
+ assertTrue(child.getLeft() > 0);
+ } else {
+ for (int i = 0; i < wrv.getChildCount(); i++) {
+ assertEquals(0, wrv.getChildAt(i).getLeft());
+ }
}
}
- }
+ });
});
}
-
private static ViewAction swipeDownFromTopRightSlowly() {
return new GeneralSwipeAction(
Swipe.SLOW, GeneralLocation.TOP_RIGHT,
diff --git a/wear/wear/src/main/java/androidx/wear/ambient/AmbientDelegate.java b/wear/wear/src/main/java/androidx/wear/ambient/AmbientDelegate.java
index fd6146c..b78af94 100644
--- a/wear/wear/src/main/java/androidx/wear/ambient/AmbientDelegate.java
+++ b/wear/wear/src/main/java/androidx/wear/ambient/AmbientDelegate.java
@@ -51,7 +51,7 @@
* method. If they do not, an exception will be thrown.</em>
*
* @param ambientDetails bundle containing information about the display being used.
- * It includes information about low-bit color and burn-in protection.
+ * It includes information about low-bit color and burn-in protection.
*/
void onEnterAmbient(Bundle ambientDetails);
@@ -81,15 +81,15 @@
}
AmbientDelegate(@Nullable Activity activity,
- @NonNull WearableControllerProvider wearableControllerProvider,
- @NonNull AmbientCallback callback) {
+ @NonNull WearableControllerProvider wearableControllerProvider,
+ @NonNull AmbientCallback callback) {
mActivity = new WeakReference<>(activity);
mCallback = callback;
mWearableControllerProvider = wearableControllerProvider;
}
/**
- * Receives and handles the onCreate call from the associated {@link AmbientMode}
+ * Receives and handles the onCreate call from the associated {@link AmbientModeSupport}
*/
void onCreate() {
Activity activity = mActivity.get();
@@ -103,7 +103,7 @@
}
/**
- * Receives and handles the onResume call from the associated {@link AmbientMode}
+ * Receives and handles the onResume call from the associated {@link AmbientModeSupport}
*/
void onResume() {
if (mWearableController != null) {
@@ -112,7 +112,7 @@
}
/**
- * Receives and handles the onPause call from the associated {@link AmbientMode}
+ * Receives and handles the onPause call from the associated {@link AmbientModeSupport}
*/
void onPause() {
if (mWearableController != null) {
@@ -121,7 +121,7 @@
}
/**
- * Receives and handles the onStop call from the associated {@link AmbientMode}
+ * Receives and handles the onStop call from the associated {@link AmbientModeSupport}
*/
void onStop() {
if (mWearableController != null) {
@@ -130,7 +130,7 @@
}
/**
- * Receives and handles the onDestroy call from the associated {@link AmbientMode}
+ * Receives and handles the onDestroy call from the associated {@link AmbientModeSupport}
*/
void onDestroy() {
if (mWearableController != null) {
@@ -156,6 +156,18 @@
}
/**
+ * Sets whether this activity's task should be moved to the front when the system exits
+ * ambient mode. If true, the activity's task may be moved to the front if it was the last
+ * activity to be running when ambient started, depending on how much time the system spent
+ * in ambient mode.
+ */
+ public void setAutoResumeEnabled(boolean enabled) {
+ if (mWearableController != null) {
+ mWearableController.setAutoResumeEnabled(enabled);
+ }
+ }
+
+ /**
* @return {@code true} if the activity is currently in ambient.
*/
boolean isAmbient() {
diff --git a/wear/wear/src/main/java/androidx/wear/ambient/AmbientModeSupport.java b/wear/wear/src/main/java/androidx/wear/ambient/AmbientModeSupport.java
index b0eebcf..bf21a45 100644
--- a/wear/wear/src/main/java/androidx/wear/ambient/AmbientModeSupport.java
+++ b/wear/wear/src/main/java/androidx/wear/ambient/AmbientModeSupport.java
@@ -47,12 +47,12 @@
* {@link FragmentActivity} and use the {@link AmbientController} can be found below:
* <p>
* <pre class="prettyprint">{@code
- * AmbientMode.AmbientController controller = AmbientMode.attachAmbientSupport(this);
- * boolean isAmbient = controller.isAmbient();
+ * AmbientModeSupport.AmbientController controller = AmbientModeSupport.attach(this);
+ * boolean isAmbient = controller.isAmbient();
* }</pre>
*/
public final class AmbientModeSupport extends Fragment {
- private static final String TAG = "AmbientMode";
+ private static final String TAG = "AmbientModeSupport";
/**
* Property in bundle passed to {@code AmbientCallback#onEnterAmbient(Bundle)} to indicate
@@ -82,11 +82,11 @@
/**
* Interface for any {@link Activity} that wishes to implement Ambient Mode. Use the
- * {@link #getAmbientCallback()} method to return and {@link AmbientCallback} which can be used
+ * {@link #getAmbientCallback()} method to return an {@link AmbientCallback} which can be used
* to bind the {@link AmbientModeSupport} to the instantiation of this interface.
* <p>
* <pre class="prettyprint">{@code
- * return new AmbientMode.AmbientCallback() {
+ * return new AmbientModeSupport.AmbientCallback() {
* public void onEnterAmbient(Bundle ambientDetails) {...}
* public void onExitAmbient(Bundle ambientDetails) {...}
* }
@@ -101,7 +101,8 @@
}
/**
- * Callback to receive ambient mode state changes. It must be used by all users of AmbientMode.
+ * Callback to receive ambient mode state changes. It must be used by all users of
+ * AmbientModeSupport.
*/
public abstract static class AmbientCallback {
/**
@@ -302,5 +303,17 @@
mDelegate.setAmbientOffloadEnabled(enabled);
}
}
+
+ /**
+ * Sets whether this activity's task should be moved to the front when the system exits
+ * ambient mode. If true, the activity's task may be moved to the front if it was the
+ * last activity to be running when ambient started, depending on how much time the
+ * system spent in ambient mode.
+ */
+ public void setAutoResumeEnabled(boolean enabled) {
+ if (mDelegate != null) {
+ mDelegate.setAutoResumeEnabled(enabled);
+ }
+ }
}
}
diff --git a/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivity.java b/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivity.java
index 6ac18bd..f2af9a3 100644
--- a/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivity.java
+++ b/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivity.java
@@ -21,6 +21,7 @@
import android.content.Context;
import android.graphics.drawable.Icon;
import android.os.Build;
+import android.os.SystemClock;
import android.service.notification.StatusBarNotification;
import androidx.annotation.DrawableRes;
@@ -83,6 +84,7 @@
private PendingIntent mTouchIntent;
private LocusIdCompat mLocusId;
private int mOngoingActivityId = OngoingActivityData.DEFAULT_ID;
+ private String mCategory;
/**
* Construct a new empty {@link Builder}, associated with the given notification.
@@ -106,6 +108,8 @@
* {@link OngoingActivity}. For example, in the WatchFace.
* Should be white with a transparent background, preferably an AnimatedVectorDrawable.
*/
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ // No getters needed on OngoingActivity - receiver will consume from OngoingActivityData.
@NonNull
public Builder setAnimatedIcon(@NonNull Icon animatedIcon) {
mAnimatedIcon = animatedIcon;
@@ -117,6 +121,8 @@
* {@link OngoingActivity}. For example, in the WatchFace.
* Should be white with a transparent background, preferably an AnimatedVectorDrawable.
*/
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ // No getters needed on OngoingActivity - receiver will consume from OngoingActivityData.
@NonNull
public Builder setAnimatedIcon(@DrawableRes int animatedIcon) {
mAnimatedIcon = Icon.createWithResource(mContext, animatedIcon);
@@ -128,6 +134,8 @@
* {@link OngoingActivity}, for example in the WatchFace in ambient mode.
* Should be white with a transparent background, preferably an VectorDrawable.
*/
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ // No getters needed on OngoingActivity - receiver will consume from OngoingActivityData.
@NonNull
public Builder setStaticIcon(@NonNull Icon staticIcon) {
mStaticIcon = staticIcon;
@@ -139,6 +147,8 @@
* {@link OngoingActivity}, for example in the WatchFace in ambient mode.
* Should be white with a transparent background, preferably an VectorDrawable.
*/
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ // No getters needed on OngoingActivity - receiver will consume from OngoingActivityData.
@NonNull
public Builder setStaticIcon(@DrawableRes int staticIcon) {
mStaticIcon = Icon.createWithResource(mContext, staticIcon);
@@ -149,6 +159,8 @@
* Set the initial status of this ongoing activity, the status may be displayed on the UI to
* show progress of the Ongoing Activity.
*/
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ // No getters needed on OngoingActivity - receiver will consume from OngoingActivityData.
@NonNull
public Builder setStatus(@NonNull OngoingActivityStatus status) {
mStatus = status;
@@ -159,6 +171,8 @@
* Set the intent to be used to go back to the activity when the user interacts with the
* Ongoing Activity in other surfaces (for example, taps the Icon on the WatchFace)
*/
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ // No getters needed on OngoingActivity - receiver will consume from OngoingActivityData.
@NonNull
public Builder setTouchIntent(@NonNull PendingIntent touchIntent) {
mTouchIntent = touchIntent;
@@ -169,6 +183,8 @@
* Set the corresponding LocusId of this {@link OngoingActivity}, this will be used by the
* launcher to identify the corresponding launcher item and display it accordingly.
*/
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ // No getters needed on OngoingActivity - receiver will consume from OngoingActivityData.
@NonNull
public Builder setLocusId(@NonNull LocusIdCompat locusId) {
mLocusId = locusId;
@@ -179,6 +195,8 @@
* Give an id to this {@link OngoingActivity}, as a way to reference it in
* {@link OngoingActivity#fromExistingOngoingActivity(Context, int)}
*/
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ // No getters needed on OngoingActivity - receiver will consume from OngoingActivityData.
@NonNull
public Builder setOngoingActivityId(int ongoingActivityId) {
mOngoingActivityId = ongoingActivityId;
@@ -186,6 +204,18 @@
}
/**
+ * Set the category of this {@link OngoingActivity}, this may be used by the system to
+ * prioritize it.
+ */
+ @SuppressWarnings("MissingGetterMatchingBuilder")
+ // No getters needed on OngoingActivity - receiver will consume from OngoingActivityData.
+ @NonNull
+ public Builder setCategory(@NonNull String category) {
+ mCategory = category;
+ return this;
+ }
+
+ /**
* Combine all options provided and the information in the notification if needed,
* return a new {@link OngoingActivity} object.
*
@@ -220,14 +250,18 @@
locusId = Api29Impl.getLocusId(notification);
}
+ String category = mCategory == null ? notification.category : mCategory;
+
return new OngoingActivity(mNotificationId, mNotificationBuilder,
new OngoingActivityData(
mAnimatedIcon,
staticIcon,
status,
touchIntent,
- locusId,
- mOngoingActivityId
+ locusId == null ? null : locusId.getId(),
+ mOngoingActivityId,
+ category,
+ SystemClock.elapsedRealtime()
));
}
}
@@ -278,8 +312,8 @@
StatusBarNotification[] notifications =
context.getSystemService(NotificationManager.class).getActiveNotifications();
for (StatusBarNotification statusBarNotification : notifications) {
- OngoingActivityData data = OngoingActivityData.create(
- statusBarNotification.getNotification());
+ OngoingActivityData data =
+ OngoingActivityData.create(statusBarNotification.getNotification());
if (data != null && filter.test(data)) {
return new OngoingActivity(statusBarNotification.getId(),
new NotificationCompat.Builder(context,
diff --git a/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivityData.java b/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivityData.java
index 17130b4..5b07295 100644
--- a/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivityData.java
+++ b/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivityData.java
@@ -18,6 +18,7 @@
import android.app.Notification;
import android.app.PendingIntent;
import android.graphics.drawable.Icon;
+import android.os.SystemClock;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -35,34 +36,47 @@
public class OngoingActivityData implements VersionedParcelable {
@Nullable
@ParcelField(value = 1, defaultValue = "null")
- private final Icon mAnimatedIcon;
+ Icon mAnimatedIcon;
@NonNull
@ParcelField(value = 2)
- private final Icon mStaticIcon;
+ Icon mStaticIcon;
@Nullable
@ParcelField(value = 3, defaultValue = "null")
- private OngoingActivityStatus mStatus;
+ OngoingActivityStatus mStatus;
@NonNull
@ParcelField(value = 4)
- private final PendingIntent mTouchIntent;
+ PendingIntent mTouchIntent;
@Nullable
@ParcelField(value = 5, defaultValue = "null")
- private final LocusIdCompat mLocusId;
+ String mLocusId;
@ParcelField(value = 6, defaultValue = "-1")
- private final int mOngoingActivityId;
+ int mOngoingActivityId;
+
+ @Nullable
+ @ParcelField(value = 7, defaultValue = "null")
+ String mCategory;
+
+ @ParcelField(value = 8)
+ long mTimestamp;
+
+ // Required by VersionedParcelable
+ OngoingActivityData() {
+ }
OngoingActivityData(
@Nullable Icon animatedIcon,
@NonNull Icon staticIcon,
@Nullable OngoingActivityStatus status,
@NonNull PendingIntent touchIntent,
- @Nullable LocusIdCompat locusId,
- int ongoingActivityId
+ @Nullable String locusId,
+ int ongoingActivityId,
+ @Nullable String category,
+ long timestamp
) {
mAnimatedIcon = animatedIcon;
mStaticIcon = staticIcon;
@@ -70,6 +84,8 @@
mTouchIntent = touchIntent;
mLocusId = locusId;
mOngoingActivityId = ongoingActivityId;
+ mCategory = category;
+ mTimestamp = timestamp;
}
@NonNull
@@ -122,7 +138,8 @@
/**
* Get the static icon that can be used on some surfaces to represent this
- * {@link OngoingActivity}. For example in the WatchFace in ambient mode.
+ * {@link OngoingActivity}. For example in the WatchFace in ambient mode. If not set, returns
+ * the small icon of the corresponding Notification.
*/
@NonNull
public Icon getStaticIcon() {
@@ -131,7 +148,8 @@
/**
* Get the status of this ongoing activity, the status may be displayed on the UI to
- * show progress of the Ongoing Activity.
+ * show progress of the Ongoing Activity. If not set, returns the content text of the
+ * corresponding Notification.
*/
@Nullable
public OngoingActivityStatus getStatus() {
@@ -140,7 +158,8 @@
/**
* Get the intent to be used to go back to the activity when the user interacts with the
- * Ongoing Activity in other surfaces (for example, taps the Icon on the WatchFace)
+ * Ongoing Activity in other surfaces (for example, taps the Icon on the WatchFace). If not
+ * set, returns the touch intent of the corresponding Notification.
*/
@NonNull
public PendingIntent getTouchIntent() {
@@ -149,11 +168,12 @@
/**
* Get the LocusId of this {@link OngoingActivity}, this can be used by the launcher to
- * identify the corresponding launcher item and display it accordingly.
+ * identify the corresponding launcher item and display it accordingly. If not set, returns
+ * the one in the corresponding Notification.
*/
@Nullable
public LocusIdCompat getLocusId() {
- return mLocusId;
+ return new LocusIdCompat(mLocusId);
}
/**
@@ -164,6 +184,22 @@
return mOngoingActivityId;
}
+ /**
+ * Get the Category of this {@link OngoingActivity} if set, otherwise the category of the
+ * corresponding notification.
+ */
+ @Nullable
+ public String getCategory() {
+ return mCategory;
+ }
+
+ /**
+ * Get the time (in {@link SystemClock#elapsedRealtime()} time) the OngoingActivity was built.
+ */
+ public long getTimestamp() {
+ return mTimestamp;
+ }
+
// Status is mutable, by the library.
void setStatus(@NonNull OngoingActivityStatus status) {
mStatus = status;
diff --git a/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivityStatus.java b/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivityStatus.java
index fc37c9d..2dc53a7 100644
--- a/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivityStatus.java
+++ b/wear/wear/src/main/java/androidx/wear/ongoingactivity/OngoingActivityStatus.java
@@ -21,6 +21,8 @@
import androidx.versionedparcelable.VersionedParcelable;
import androidx.versionedparcelable.VersionedParcelize;
+import kotlin.NotImplementedError;
+
/**
* Base class to serialize / deserialize {@link OngoingActivityStatus} into / from a Notification
*
@@ -29,7 +31,12 @@
* {@link android.os.SystemClock#elapsedRealtime()}
*/
@VersionedParcelize
-public abstract class OngoingActivityStatus implements VersionedParcelable {
+public class OngoingActivityStatus implements VersionedParcelable {
+
+ // Required by VersionedParcelable
+ OngoingActivityStatus() {
+ }
+
/**
* Returns a textual representation of the ongoing activity status at the given time
* represented as milliseconds timestamp
@@ -43,7 +50,9 @@
* returned by {@link android.os.SystemClock#elapsedRealtime()}.
*/
@NonNull
- public abstract CharSequence getText(@NonNull Context context, long timeNowMillis);
+ public CharSequence getText(@NonNull Context context, long timeNowMillis) {
+ throw new NotImplementedError();
+ }
/**
* Returns the timestamp of the next time when the display will be different from the current
@@ -56,7 +65,9 @@
* @return the first point in time after {@code fromTimeMillis} when the displayed value of
* this status will change. returns Long.MAX_VALUE if the display will never change.
*/
- public abstract long getNextChangeTimeMillis(long fromTimeMillis);
+ public long getNextChangeTimeMillis(long fromTimeMillis) {
+ throw new NotImplementedError();
+ }
// Invalid value to use for paused_at and duration, as suggested by api guidelines 5.15
static final long LONG_DEFAULT = -1L;
diff --git a/wear/wear/src/main/java/androidx/wear/ongoingactivity/TextOngoingActivityStatus.java b/wear/wear/src/main/java/androidx/wear/ongoingactivity/TextOngoingActivityStatus.java
index bceaded..d31e814 100644
--- a/wear/wear/src/main/java/androidx/wear/ongoingactivity/TextOngoingActivityStatus.java
+++ b/wear/wear/src/main/java/androidx/wear/ongoingactivity/TextOngoingActivityStatus.java
@@ -31,7 +31,11 @@
public class TextOngoingActivityStatus extends OngoingActivityStatus {
@NonNull
@ParcelField(value = 1, defaultValue = "")
- private String mStr = "";
+ String mStr = "";
+
+ // Required by VersionedParcelable
+ TextOngoingActivityStatus() {
+ }
public TextOngoingActivityStatus(@NonNull String str) {
this.mStr = str;
diff --git a/wear/wear/src/main/java/androidx/wear/ongoingactivity/TimerOngoingActivityStatus.java b/wear/wear/src/main/java/androidx/wear/ongoingactivity/TimerOngoingActivityStatus.java
index 524423e..79d76df 100644
--- a/wear/wear/src/main/java/androidx/wear/ongoingactivity/TimerOngoingActivityStatus.java
+++ b/wear/wear/src/main/java/androidx/wear/ongoingactivity/TimerOngoingActivityStatus.java
@@ -20,6 +20,7 @@
import android.text.format.DateUtils;
import androidx.annotation.NonNull;
+import androidx.versionedparcelable.NonParcelField;
import androidx.versionedparcelable.ParcelField;
import androidx.versionedparcelable.VersionedParcelize;
@@ -31,20 +32,26 @@
@VersionedParcelize
public class TimerOngoingActivityStatus extends OngoingActivityStatus {
@ParcelField(value = 1, defaultValue = "0")
- private long mTimeZeroMillis;
+ long mTimeZeroMillis;
@ParcelField(value = 2, defaultValue = "false")
- private boolean mCountDown = false;
+ boolean mCountDown = false;
@ParcelField(value = 3, defaultValue = "-1")
- private long mPausedAtMillis = LONG_DEFAULT;
+ long mPausedAtMillis = LONG_DEFAULT;
@ParcelField(value = 4, defaultValue = "-1")
- private long mTotalDurationMillis = LONG_DEFAULT;
+ long mTotalDurationMillis = LONG_DEFAULT;
+ @NonParcelField
private final StringBuilder mStringBuilder = new StringBuilder(8);
+
private static final String NEGATIVE_DURATION_PREFIX = "-";
+ // Required by VersionedParcelable
+ TimerOngoingActivityStatus() {
+ }
+
/**
* Create a Status representing a timer or stopwatch.
*
diff --git a/window/window-extensions/api/current.txt b/window/window-extensions/api/current.txt
index 01608ca..78bdc7f 100644
--- a/window/window-extensions/api/current.txt
+++ b/window/window-extensions/api/current.txt
@@ -11,10 +11,18 @@
field public static final int POSTURE_UNKNOWN = 0; // 0x0
}
- public class ExtensionDisplayFeature {
- ctor public ExtensionDisplayFeature(android.graphics.Rect, int);
+ public interface ExtensionDisplayFeature {
method public android.graphics.Rect getBounds();
+ }
+
+ public class ExtensionFoldingFeature implements androidx.window.extensions.ExtensionDisplayFeature {
+ ctor public ExtensionFoldingFeature(android.graphics.Rect, int, int);
+ method public android.graphics.Rect getBounds();
+ method public int getState();
method public int getType();
+ field public static final int STATE_FLAT = 1; // 0x1
+ field public static final int STATE_FLIPPED = 3; // 0x3
+ field public static final int STATE_HALF_OPENED = 2; // 0x2
field public static final int TYPE_FOLD = 1; // 0x1
field public static final int TYPE_HINGE = 2; // 0x2
}
diff --git a/window/window-extensions/api/public_plus_experimental_current.txt b/window/window-extensions/api/public_plus_experimental_current.txt
index 01608ca..78bdc7f 100644
--- a/window/window-extensions/api/public_plus_experimental_current.txt
+++ b/window/window-extensions/api/public_plus_experimental_current.txt
@@ -11,10 +11,18 @@
field public static final int POSTURE_UNKNOWN = 0; // 0x0
}
- public class ExtensionDisplayFeature {
- ctor public ExtensionDisplayFeature(android.graphics.Rect, int);
+ public interface ExtensionDisplayFeature {
method public android.graphics.Rect getBounds();
+ }
+
+ public class ExtensionFoldingFeature implements androidx.window.extensions.ExtensionDisplayFeature {
+ ctor public ExtensionFoldingFeature(android.graphics.Rect, int, int);
+ method public android.graphics.Rect getBounds();
+ method public int getState();
method public int getType();
+ field public static final int STATE_FLAT = 1; // 0x1
+ field public static final int STATE_FLIPPED = 3; // 0x3
+ field public static final int STATE_HALF_OPENED = 2; // 0x2
field public static final int TYPE_FOLD = 1; // 0x1
field public static final int TYPE_HINGE = 2; // 0x2
}
diff --git a/window/window-extensions/api/restricted_current.txt b/window/window-extensions/api/restricted_current.txt
index 01608ca..78bdc7f 100644
--- a/window/window-extensions/api/restricted_current.txt
+++ b/window/window-extensions/api/restricted_current.txt
@@ -11,10 +11,18 @@
field public static final int POSTURE_UNKNOWN = 0; // 0x0
}
- public class ExtensionDisplayFeature {
- ctor public ExtensionDisplayFeature(android.graphics.Rect, int);
+ public interface ExtensionDisplayFeature {
method public android.graphics.Rect getBounds();
+ }
+
+ public class ExtensionFoldingFeature implements androidx.window.extensions.ExtensionDisplayFeature {
+ ctor public ExtensionFoldingFeature(android.graphics.Rect, int, int);
+ method public android.graphics.Rect getBounds();
+ method public int getState();
method public int getType();
+ field public static final int STATE_FLAT = 1; // 0x1
+ field public static final int STATE_FLIPPED = 3; // 0x3
+ field public static final int STATE_HALF_OPENED = 2; // 0x2
field public static final int TYPE_FOLD = 1; // 0x1
field public static final int TYPE_HINGE = 2; // 0x2
}
diff --git a/window/window-extensions/build.gradle b/window/window-extensions/build.gradle
index 82c95ec..6e84dbc 100644
--- a/window/window-extensions/build.gradle
+++ b/window/window-extensions/build.gradle
@@ -19,6 +19,13 @@
import androidx.build.Publish
import androidx.build.RunApiTasks
+import static androidx.build.dependencies.DependenciesKt.ANDROIDX_TEST_EXT_JUNIT
+import static androidx.build.dependencies.DependenciesKt.ANDROIDX_TEST_RULES
+import static androidx.build.dependencies.DependenciesKt.ANDROIDX_TEST_RUNNER
+import static androidx.build.dependencies.DependenciesKt.DEXMAKER_MOCKITO
+import static androidx.build.dependencies.DependenciesKt.MOCKITO_CORE
+import static androidx.build.dependencies.DependenciesKt.TRUTH
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
@@ -32,6 +39,12 @@
dependencies {
implementation("androidx.annotation:annotation:1.1.0")
+
+ androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
+ androidTestImplementation(ANDROIDX_TEST_RUNNER)
+ androidTestImplementation(ANDROIDX_TEST_RULES)
+ androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy)
+ androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy)
}
androidx {
diff --git a/car/app/app/src/androidTest/AndroidManifest.xml b/window/window-extensions/src/androidTest/AndroidManifest.xml
similarity index 68%
copy from car/app/app/src/androidTest/AndroidManifest.xml
copy to window/window-extensions/src/androidTest/AndroidManifest.xml
index 3bc2684..d85d231 100644
--- a/car/app/app/src/androidTest/AndroidManifest.xml
+++ b/window/window-extensions/src/androidTest/AndroidManifest.xml
@@ -15,5 +15,11 @@
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="androidx.car.app">
+ package="androidx.window.extensions.test">
+
+ <application>
+ <activity android:name="androidx.window.extensions.TestActivity" />
+ <activity android:name="androidx.window.extensions.TestConfigChangeHandlingActivity"
+ android:configChanges="orientation|screenLayout|screenSize"/>
+ </application>
</manifest>
diff --git a/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionDeviceStateTest.java b/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionDeviceStateTest.java
new file mode 100644
index 0000000..3299a08
--- /dev/null
+++ b/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionDeviceStateTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.extensions;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link ExtensionDeviceState} class. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ExtensionDeviceStateTest {
+
+ @Test
+ public void testEquals_samePosture() {
+ ExtensionDeviceState original = new ExtensionDeviceState(0);
+ ExtensionDeviceState copy = new ExtensionDeviceState(0);
+
+ assertEquals(original, copy);
+ }
+
+ @Test
+ public void testEquals_differentPosture() {
+ ExtensionDeviceState original = new ExtensionDeviceState(0);
+ ExtensionDeviceState different = new ExtensionDeviceState(1);
+
+ assertNotEquals(original, different);
+ }
+
+ @Test
+ public void testHashCode_matchesIfEqual() {
+ int posture = 111;
+ ExtensionDeviceState original = new ExtensionDeviceState(posture);
+ ExtensionDeviceState matching = new ExtensionDeviceState(posture);
+
+ assertEquals(original, matching);
+ assertEquals(original.hashCode(), matching.hashCode());
+ }
+}
diff --git a/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionDisplayFeatureTest.java b/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionDisplayFeatureTest.java
new file mode 100644
index 0000000..fdd42fa
--- /dev/null
+++ b/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionDisplayFeatureTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.extensions;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.graphics.Rect;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link ExtensionFoldingFeature} class. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ExtensionDisplayFeatureTest {
+
+ @Test
+ public void testEquals_sameAttributes() {
+ Rect bounds = new Rect(1, 0, 1, 10);
+ int type = ExtensionFoldingFeature.TYPE_FOLD;
+ int state = ExtensionFoldingFeature.STATE_FLAT;
+
+ ExtensionFoldingFeature original = new ExtensionFoldingFeature(bounds, type, state);
+ ExtensionFoldingFeature copy = new ExtensionFoldingFeature(bounds, type, state);
+
+ assertEquals(original, copy);
+ }
+
+ @Test
+ public void testEquals_differentRect() {
+ Rect originalRect = new Rect(1, 0, 1, 10);
+ Rect otherRect = new Rect(2, 0, 2, 10);
+ int type = ExtensionFoldingFeature.TYPE_FOLD;
+ int state = ExtensionFoldingFeature.STATE_FLAT;
+
+ ExtensionFoldingFeature original = new ExtensionFoldingFeature(originalRect, type,
+ state);
+ ExtensionFoldingFeature other = new ExtensionFoldingFeature(otherRect, type, state);
+
+ assertNotEquals(original, other);
+ }
+
+ @Test
+ public void testEquals_differentType() {
+ Rect rect = new Rect(1, 0, 1, 10);
+ int originalType = ExtensionFoldingFeature.TYPE_FOLD;
+ int otherType = ExtensionFoldingFeature.TYPE_HINGE;
+ int state = ExtensionFoldingFeature.STATE_FLAT;
+
+ ExtensionFoldingFeature original = new ExtensionFoldingFeature(rect, originalType,
+ state);
+ ExtensionFoldingFeature other = new ExtensionFoldingFeature(rect, otherType, state);
+
+ assertNotEquals(original, other);
+ }
+
+ @Test
+ public void testEquals_differentState() {
+ Rect rect = new Rect(1, 0, 1, 10);
+ int type = ExtensionFoldingFeature.TYPE_FOLD;
+ int originalState = ExtensionFoldingFeature.STATE_FLAT;
+ int otherState = ExtensionFoldingFeature.STATE_FLIPPED;
+
+ ExtensionFoldingFeature original = new ExtensionFoldingFeature(rect, type,
+ originalState);
+ ExtensionFoldingFeature other = new ExtensionFoldingFeature(rect, type, otherState);
+
+ assertNotEquals(original, other);
+ }
+
+ @Test
+ public void testHashCode_matchesIfEqual() {
+ Rect originalRect = new Rect(1, 0, 1, 10);
+ Rect matchingRect = new Rect(1, 0, 1, 10);
+ int type = ExtensionFoldingFeature.TYPE_FOLD;
+ int state = ExtensionFoldingFeature.STATE_FLAT;
+
+ ExtensionFoldingFeature original = new ExtensionFoldingFeature(originalRect, type,
+ state);
+ ExtensionFoldingFeature matching = new ExtensionFoldingFeature(matchingRect, type,
+ state);
+
+ assertEquals(original, matching);
+ assertEquals(original.hashCode(), matching.hashCode());
+ }
+}
diff --git a/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionWindowLayoutInfoTest.java b/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionWindowLayoutInfoTest.java
new file mode 100644
index 0000000..bb87c3b
--- /dev/null
+++ b/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionWindowLayoutInfoTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.extensions;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.graphics.Rect;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Tests for {@link ExtensionWindowLayoutInfo} class. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ExtensionWindowLayoutInfoTest {
+
+ @Test
+ public void testEquals_sameFeatures() {
+ List<ExtensionDisplayFeature> features = new ArrayList<>();
+
+ ExtensionWindowLayoutInfo original = new ExtensionWindowLayoutInfo(features);
+ ExtensionWindowLayoutInfo copy = new ExtensionWindowLayoutInfo(features);
+
+ assertEquals(original, copy);
+ }
+
+ @Test
+ public void testEquals_differentFeatures() {
+ List<ExtensionDisplayFeature> originalFeatures = new ArrayList<>();
+ List<ExtensionDisplayFeature> differentFeatures = new ArrayList<>();
+ Rect rect = new Rect(1, 0, 1, 10);
+ differentFeatures.add(new ExtensionFoldingFeature(
+ rect, ExtensionFoldingFeature.TYPE_HINGE,
+ ExtensionFoldingFeature.STATE_FLAT));
+
+ ExtensionWindowLayoutInfo original = new ExtensionWindowLayoutInfo(originalFeatures);
+ ExtensionWindowLayoutInfo different = new ExtensionWindowLayoutInfo(differentFeatures);
+
+ assertNotEquals(original, different);
+ }
+
+ @Test
+ public void testHashCode_matchesIfEqual() {
+ List<ExtensionDisplayFeature> firstFeatures = new ArrayList<>();
+ List<ExtensionDisplayFeature> secondFeatures = new ArrayList<>();
+ ExtensionWindowLayoutInfo first = new ExtensionWindowLayoutInfo(firstFeatures);
+ ExtensionWindowLayoutInfo second = new ExtensionWindowLayoutInfo(secondFeatures);
+
+ assertEquals(first, second);
+ assertEquals(first.hashCode(), second.hashCode());
+ }
+
+ @Test
+ public void testHashCode_matchesIfEqualFeatures() {
+ ExtensionDisplayFeature originalFeature = new ExtensionFoldingFeature(
+ new Rect(0, 0, 100, 0),
+ ExtensionFoldingFeature.TYPE_HINGE,
+ ExtensionFoldingFeature.STATE_FLAT
+ );
+ ExtensionDisplayFeature matchingFeature = new ExtensionFoldingFeature(
+ new Rect(0, 0, 100, 0),
+ ExtensionFoldingFeature.TYPE_HINGE,
+ ExtensionFoldingFeature.STATE_FLAT
+ );
+ List<ExtensionDisplayFeature> firstFeatures = Collections.singletonList(originalFeature);
+ List<ExtensionDisplayFeature> secondFeatures = Collections.singletonList(matchingFeature);
+ ExtensionWindowLayoutInfo first = new ExtensionWindowLayoutInfo(firstFeatures);
+ ExtensionWindowLayoutInfo second = new ExtensionWindowLayoutInfo(secondFeatures);
+
+ assertEquals(first, second);
+ assertEquals(first.hashCode(), second.hashCode());
+ }
+}
diff --git a/window/window-extensions/src/androidTest/java/androidx/window/extensions/TestActivity.java b/window/window-extensions/src/androidTest/java/androidx/window/extensions/TestActivity.java
new file mode 100644
index 0000000..1c36baf
--- /dev/null
+++ b/window/window-extensions/src/androidTest/java/androidx/window/extensions/TestActivity.java
@@ -0,0 +1,55 @@
+/*
+ * 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.extensions;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import java.util.concurrent.CountDownLatch;
+
+public class TestActivity extends Activity implements View.OnLayoutChangeListener {
+
+ private int mRootViewId;
+ private CountDownLatch mLayoutLatch = new CountDownLatch(1);
+ private static CountDownLatch sResumeLatch = new CountDownLatch(1);
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ final View contentView = new View(this);
+ mRootViewId = View.generateViewId();
+ contentView.setId(mRootViewId);
+ setContentView(contentView);
+
+ getWindow().getDecorView().addOnLayoutChangeListener(this);
+ }
+
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
+ int oldTop, int oldRight, int oldBottom) {
+ mLayoutLatch.countDown();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ sResumeLatch.countDown();
+ }
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt b/window/window-extensions/src/androidTest/java/androidx/window/extensions/TestConfigChangeHandlingActivity.java
similarity index 77%
copy from compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
copy to window/window-extensions/src/androidTest/java/androidx/window/extensions/TestConfigChangeHandlingActivity.java
index f9cb2fe..f0a2871 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/ExperimentalFocus.kt
+++ b/window/window-extensions/src/androidTest/java/androidx/window/extensions/TestConfigChangeHandlingActivity.java
@@ -14,7 +14,8 @@
* limitations under the License.
*/
-package androidx.compose.ui.focus
+package androidx.window.extensions;
-@RequiresOptIn("The Focus API is experimental and is likely to change in the future.")
-annotation class ExperimentalFocus
\ No newline at end of file
+/** Activity that handles orientation configuration change. */
+public final class TestConfigChangeHandlingActivity extends TestActivity {
+}
diff --git a/window/window-extensions/src/androidTest/java/androidx/window/extensions/WindowTestBase.java b/window/window-extensions/src/androidTest/java/androidx/window/extensions/WindowTestBase.java
new file mode 100644
index 0000000..b68fc23f
--- /dev/null
+++ b/window/window-extensions/src/androidTest/java/androidx/window/extensions/WindowTestBase.java
@@ -0,0 +1,40 @@
+/*
+ * 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.extensions;
+
+import android.app.Activity;
+import android.os.IBinder;
+
+import androidx.test.core.app.ActivityScenario;
+
+import org.junit.Before;
+
+/**
+ * Base class for all tests in the module.
+ */
+class WindowTestBase {
+ ActivityScenario<TestActivity> mActivityTestRule;
+
+ @Before
+ public void setUp() {
+ mActivityTestRule = ActivityScenario.launch(TestActivity.class);
+ }
+
+ static IBinder getActivityWindowToken(Activity activity) {
+ return activity.getWindow().getAttributes().token;
+ }
+}
diff --git a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionDeviceState.java b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionDeviceState.java
index be7ce85..f2f1481 100644
--- a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionDeviceState.java
+++ b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionDeviceState.java
@@ -43,8 +43,6 @@
@Retention(RetentionPolicy.SOURCE)
@IntDef({
- POSTURE_UNKNOWN,
- POSTURE_CLOSED,
POSTURE_HALF_OPENED,
POSTURE_OPENED,
POSTURE_FLIPPED
diff --git a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionDisplayFeature.java b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionDisplayFeature.java
index 8bbd56d..59794d9 100644
--- a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionDisplayFeature.java
+++ b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionDisplayFeature.java
@@ -18,110 +18,19 @@
import android.graphics.Rect;
-import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
/**
* Description of a physical feature on the display.
*/
-public class ExtensionDisplayFeature {
+public interface ExtensionDisplayFeature {
+
/**
- * The bounding rectangle of the feature within the application window in the window
- * coordinate space.
+ * The bounding rectangle of the feature within the application window
+ * in the window coordinate space.
*
- * <p>The bounds for features of type {@link #TYPE_FOLD fold} must be zero-high (for
- * horizontal folds) or zero-wide (for vertical folds) and span the entire window.
- *
- * <p>The bounds for features of type {@link #TYPE_HINGE hinge} must span the entire window
- * but, unlike folds, can have a non-zero area which represents the region that is occluded by
- * the hinge and not visible to the user.
+ * @return bounds of display feature.
*/
@NonNull
- private final Rect mBounds;
-
- /**
- * The physical type of the feature.
- */
- @Type
- private final int mType;
-
- /**
- * A fold in the flexible screen without a physical gap.
- */
- public static final int TYPE_FOLD = 1;
-
- /**
- * A physical separation with a hinge that allows two display panels to fold.
- */
- public static final int TYPE_HINGE = 2;
-
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({
- TYPE_FOLD,
- TYPE_HINGE,
- })
- @interface Type{}
-
- public ExtensionDisplayFeature(@NonNull Rect bounds, @Type int type) {
- mBounds = new Rect(bounds);
- mType = type;
- }
-
- /** Gets the bounding rect of the display feature in window coordinate space. */
- @NonNull
- public Rect getBounds() {
- return new Rect(mBounds);
- }
-
- /** Gets the type of the display feature. */
- @Type
- public int getType() {
- return mType;
- }
-
- @NonNull
- private static String typeToString(int type) {
- switch (type) {
- case TYPE_FOLD:
- return "FOLD";
- case TYPE_HINGE:
- return "HINGE";
- default:
- return "Unknown feature type (" + type + ")";
- }
- }
-
- @NonNull
- @Override
- public String toString() {
- return "ExtensionDisplayFeature { bounds=" + mBounds + ", type=" + typeToString(getType())
- + " }";
- }
-
- @Override
- public boolean equals(@Nullable Object obj) {
- if (this == obj) {
- return true;
- }
- if (!(obj instanceof ExtensionDisplayFeature)) {
- return false;
- }
- final ExtensionDisplayFeature
- other = (ExtensionDisplayFeature) obj;
- if (mType != other.mType) {
- return false;
- }
- return mBounds.equals(other.mBounds);
- }
-
- @Override
- public int hashCode() {
- int result = mType;
- result = 31 * result + mBounds.centerX() + mBounds.centerY();
- return result;
- }
+ Rect getBounds();
}
diff --git a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionFoldingFeature.java b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionFoldingFeature.java
new file mode 100644
index 0000000..e39e5ca
--- /dev/null
+++ b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionFoldingFeature.java
@@ -0,0 +1,213 @@
+/*
+ * 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.extensions;
+
+import android.graphics.Rect;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A feature that describes a fold in a flexible display
+ * or a hinge between two physical display panels.
+ */
+public class ExtensionFoldingFeature implements ExtensionDisplayFeature {
+
+ /**
+ * A fold in the flexible screen without a physical gap.
+ */
+ public static final int TYPE_FOLD = 1;
+
+ /**
+ * A physical separation with a hinge that allows two display panels to fold.
+ */
+ public static final int TYPE_HINGE = 2;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ TYPE_FOLD,
+ TYPE_HINGE,
+ })
+ @interface Type{}
+
+ /**
+ * The foldable device's hinge is completely open, the screen space that is presented to the
+ * user is flat. See the
+ * <a href="https://developer.android.com/guide/topics/ui/foldables#postures">Posture</a>
+ * section in the official documentation for visual samples and references.
+ */
+ public static final int STATE_FLAT = 1;
+
+ /**
+ * The foldable device's hinge is in an intermediate position between opened and closed state,
+ * there is a non-flat angle between parts of the flexible screen or between physical screen
+ * panels. See the
+ * <a href="https://developer.android.com/guide/topics/ui/foldables#postures">Posture</a>
+ * section in the official documentation for visual samples and references.
+ */
+ public static final int STATE_HALF_OPENED = 2;
+
+ /**
+ * The foldable device's hinge is flipped with the flexible screen parts or physical screens
+ * facing opposite directions. See the
+ * <a href="https://developer.android.com/guide/topics/ui/foldables#postures">Posture</a>
+ * section in the official documentation for visual samples and references.
+ */
+ public static final int STATE_FLIPPED = 3;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ STATE_HALF_OPENED,
+ STATE_FLAT,
+ STATE_FLIPPED
+ })
+ @interface State {}
+
+ /**
+ * The bounding rectangle of the feature within the application window in the window
+ * coordinate space.
+ */
+ @NonNull
+ private final Rect mBounds;
+
+ /**
+ * The physical type of the feature.
+ */
+ @Type
+ private final int mType;
+
+ /**
+ * The state of the feature.
+ */
+ @State
+ private final int mState;
+
+ public ExtensionFoldingFeature(@NonNull Rect bounds, @Type int type, @State int state) {
+ validateFeatureBounds(bounds, type);
+ mBounds = new Rect(bounds);
+ mType = type;
+ mState = state;
+ }
+
+ /** Gets the bounding rect of the display feature in window coordinate space. */
+ @NonNull
+ @Override
+ public Rect getBounds() {
+ return new Rect(mBounds);
+ }
+
+ /** Gets the type of the display feature. */
+ @Type
+ public int getType() {
+ return mType;
+ }
+
+ /** Gets the state of the display feature. */
+ @State
+ public int getState() {
+ return mState;
+ }
+
+ /**
+ * Verifies the bounds of the folding feature.
+ */
+ private static void validateFeatureBounds(@NonNull Rect bounds, int type) {
+ if (bounds.width() == 0 && bounds.height() == 0) {
+ throw new IllegalArgumentException("Bounds must be non zero");
+ }
+ if (type == TYPE_FOLD) {
+ if (bounds.width() != 0 && bounds.height() != 0) {
+ throw new IllegalArgumentException("Bounding rectangle must be either zero-wide "
+ + "or zero-high for features of type " + typeToString(type));
+ }
+
+ if ((bounds.width() != 0 && bounds.left != 0)
+ || (bounds.height() != 0 && bounds.top != 0)) {
+ throw new IllegalArgumentException("Bounding rectangle must span the entire "
+ + "window space for features of type " + typeToString(type));
+ }
+ } else if (type == TYPE_HINGE) {
+ if (bounds.left != 0 && bounds.top != 0) {
+ throw new IllegalArgumentException("Bounding rectangle must span the entire "
+ + "window space for features of type " + typeToString(type));
+ }
+ }
+ }
+
+ @NonNull
+ private static String typeToString(int type) {
+ switch (type) {
+ case TYPE_FOLD:
+ return "FOLD";
+ case TYPE_HINGE:
+ return "HINGE";
+ default:
+ return "Unknown feature type (" + type + ")";
+ }
+ }
+
+ @NonNull
+ private static String stateToString(int state) {
+ switch (state) {
+ case STATE_FLAT:
+ return "FLAT";
+ case STATE_FLIPPED:
+ return "FLIPPED";
+ case STATE_HALF_OPENED:
+ return "HALF_OPENED";
+ default:
+ return "Unknown feature state (" + state + ")";
+ }
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "ExtensionDisplayFoldFeature { " + mBounds
+ + ", type=" + typeToString(getType()) + ", state=" + stateToString(mState) + " }";
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof ExtensionFoldingFeature)) {
+ return false;
+ }
+ final ExtensionFoldingFeature other = (ExtensionFoldingFeature) obj;
+ if (mType != other.mType) {
+ return false;
+ }
+ if (mState != other.mState) {
+ return false;
+ }
+ return mBounds.equals(other.mBounds);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mBounds.hashCode();
+ result = 31 * result + mType;
+ result = 31 * result + mState;
+ return result;
+ }
+}
diff --git a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionWindowLayoutInfo.java b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionWindowLayoutInfo.java
index 69e7238..0afefc7 100644
--- a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionWindowLayoutInfo.java
+++ b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionWindowLayoutInfo.java
@@ -16,8 +16,6 @@
package androidx.window.extensions;
-import android.content.Context;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -33,7 +31,6 @@
* List of display features within the window.
* <p>NOTE: All display features returned with this container must be cropped to the application
* window and reported within the coordinate space of the window that was provided by the app.
- * @see ExtensionInterface#getWindowLayoutInfo(Context)
*/
@NonNull
private List<ExtensionDisplayFeature> mDisplayFeatures;
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 77bc4de..5710f79 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
@@ -22,8 +22,7 @@
import android.widget.FrameLayout
import android.widget.TextView
import androidx.core.util.Consumer
-import androidx.window.DeviceState
-import androidx.window.DisplayFeature
+import androidx.window.FoldingFeature
import androidx.window.WindowLayoutInfo
import androidx.window.WindowManager
import java.text.SimpleDateFormat
@@ -53,20 +52,12 @@
override fun onStart() {
super.onStart()
- windowManager.registerDeviceStateChangeCallback(
- mainThreadExecutor,
- stateContainer.stateConsumer
- )
- windowManager.registerLayoutChangeCallback(
- mainThreadExecutor,
- stateContainer.layoutConsumer
- )
+ windowManager.registerLayoutChangeCallback(mainThreadExecutor, stateContainer)
}
override fun onStop() {
super.onStop()
- windowManager.unregisterDeviceStateChangeCallback(stateContainer.stateConsumer)
- windowManager.unregisterLayoutChangeCallback(stateContainer.layoutConsumer)
+ windowManager.unregisterLayoutChangeCallback(stateContainer)
}
/** Updates the device state and display feature positions. */
@@ -80,13 +71,6 @@
// Update the UI with the current state
val stateStringBuilder = StringBuilder()
- // Update the current state string
- stateContainer.lastState?.let { deviceState ->
- stateStringBuilder.append(getString(R.string.deviceState))
- .append(": ")
- .append(deviceState)
- .append("\n")
- }
stateContainer.lastLayoutInfo?.let { windowLayoutInfo ->
stateStringBuilder.append(getString(R.string.windowLayout))
@@ -107,9 +91,10 @@
}
val featureView = View(this)
- val color = when (displayFeature.type) {
- DisplayFeature.TYPE_FOLD -> getColor(R.color.colorFeatureFold)
- DisplayFeature.TYPE_HINGE -> getColor(R.color.colorFeatureHinge)
+ val foldFeature = displayFeature as? FoldingFeature
+ val color = when (foldFeature?.type) {
+ FoldingFeature.TYPE_FOLD -> getColor(R.color.colorFeatureFold)
+ FoldingFeature.TYPE_HINGE -> getColor(R.color.colorFeatureHinge)
else -> getColor(R.color.colorFeatureUnknown)
}
featureView.foreground = ColorDrawable(color)
@@ -139,25 +124,10 @@
return currentDate.toString()
}
- inner class StateContainer {
- var lastState: DeviceState? = null
+ inner class StateContainer : Consumer<WindowLayoutInfo> {
var lastLayoutInfo: WindowLayoutInfo? = null
- val stateConsumer: Consumer<DeviceState>
- val layoutConsumer: Consumer<WindowLayoutInfo>
-
- init {
- stateConsumer = Consumer { state: DeviceState -> update(state) }
- layoutConsumer = Consumer { layout: WindowLayoutInfo -> update(layout) }
- }
-
- fun update(newDeviceState: DeviceState) {
- updateStateLog(newDeviceState)
- lastState = newDeviceState
- updateCurrentState()
- }
-
- fun update(newLayoutInfo: WindowLayoutInfo) {
+ 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 5e43a73..2cc38db 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
@@ -28,7 +28,8 @@
import android.widget.TextView
import android.widget.Toast
import androidx.core.util.Consumer
-import androidx.window.DeviceState
+import androidx.window.FoldingFeature
+import androidx.window.WindowLayoutInfo
import androidx.window.WindowManager
/**
@@ -39,7 +40,7 @@
private val TAG = "FoldablePresentation"
private lateinit var windowManager: WindowManager
- private val deviceStateChangeCallback = DeviceStateChangeCallback()
+ private val deviceStateChangeCallback = WindowLayoutInfoChangeCallback()
private var presentation: DemoPresentation? = null
override fun onCreate(savedInstanceState: Bundle?) {
@@ -48,7 +49,7 @@
windowManager = getTestBackend()?.let { backend -> WindowManager(this, backend) }
?: WindowManager(this)
- windowManager.registerDeviceStateChangeCallback(
+ windowManager.registerLayoutChangeCallback(
mainThreadExecutor,
deviceStateChangeCallback
)
@@ -56,7 +57,7 @@
override fun onDestroy() {
super.onDestroy()
- windowManager.unregisterDeviceStateChangeCallback(deviceStateChangeCallback)
+ windowManager.unregisterLayoutChangeCallback(deviceStateChangeCallback)
}
internal fun startPresentation(context: Context) {
@@ -137,26 +138,36 @@
}
/**
- * Updates the display of the current device state.
+ * Updates the display of the current fold feature state.
*/
- internal fun updateCurrentState(deviceState: DeviceState) {
+ internal fun updateCurrentState(info: WindowLayoutInfo) {
val stateStringBuilder = StringBuilder()
+
stateStringBuilder.append(getString(R.string.deviceState))
.append(": ")
- .append(deviceState)
- .append("\n")
+
+ info.displayFeatures
+ .mapNotNull { it as? FoldingFeature }
+ .forEach { feature ->
+ stateStringBuilder.append(feature.stateString())
+ .append("\n")
+ }
findViewById<TextView>(R.id.currentState).text = stateStringBuilder.toString()
}
- inner class DeviceStateChangeCallback : Consumer<DeviceState> {
- override fun accept(newDeviceState: DeviceState) {
- updateCurrentState(newDeviceState)
- if (newDeviceState.posture == DeviceState.POSTURE_CLOSED) {
- startPresentation(this@PresentationActivity)
- } else {
- stopPresentation(null)
- }
+ private fun FoldingFeature.stateString(): String {
+ return when (state) {
+ FoldingFeature.STATE_FLAT -> "FLAT"
+ FoldingFeature.STATE_FLIPPED -> "FLIPPED"
+ FoldingFeature.STATE_HALF_OPENED -> "HALF_OPENED"
+ else -> "Unknown feature state ($state)"
+ }
+ }
+
+ inner class WindowLayoutInfoChangeCallback : Consumer<WindowLayoutInfo> {
+ override fun accept(info: WindowLayoutInfo) {
+ updateCurrentState(info)
}
}
}
diff --git a/window/window-samples/src/main/java/androidx/window/sample/SplitLayout.kt b/window/window-samples/src/main/java/androidx/window/sample/SplitLayout.kt
index 954c798..cc0b341 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/SplitLayout.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/SplitLayout.kt
@@ -23,8 +23,10 @@
import android.view.View.MeasureSpec.AT_MOST
import android.view.View.MeasureSpec.EXACTLY
import android.widget.FrameLayout
-import androidx.window.DisplayFeature.TYPE_FOLD
-import androidx.window.DisplayFeature.TYPE_HINGE
+import androidx.window.DisplayFeature
+import androidx.window.FoldingFeature
+import androidx.window.FoldingFeature.TYPE_FOLD
+import androidx.window.FoldingFeature.TYPE_HINGE
import androidx.window.WindowLayoutInfo
/**
@@ -41,9 +43,11 @@
private var lastHeightMeasureSpec: Int = 0
constructor(context: Context) : super(context)
+
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
setAttributes(attrs)
}
+
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
@@ -125,46 +129,43 @@
val paddedWidth = width - paddingLeft - paddingRight
val paddedHeight = height - paddingTop - paddingBottom
- for (feature in windowLayoutInfo?.displayFeatures!!) {
- // Only a hinge or a fold can split the area in two
- if (feature.type != TYPE_FOLD && feature.type != TYPE_HINGE) {
- continue
- }
+ windowLayoutInfo?.displayFeatures
+ ?.firstOrNull { feature -> isValidFoldFeature(feature) }
+ ?.let { feature ->
+ getFeaturePositionInViewRect(feature, this)?.let {
+ if (feature.bounds.left == 0) { // Horizontal layout
+ val topRect = Rect(
+ paddingLeft, paddingTop,
+ paddingLeft + paddedWidth, it.top
+ )
+ val bottomRect = Rect(
+ paddingLeft, it.bottom,
+ paddingLeft + paddedWidth, paddingTop + paddedHeight
+ )
- val splitRect = getFeaturePositionInViewRect(feature, this) ?: continue
+ if (measureAndCheckMinSize(topRect, startView) &&
+ measureAndCheckMinSize(bottomRect, endView)
+ ) {
+ return arrayOf(topRect, bottomRect)
+ }
+ } else if (feature.bounds.top == 0) { // Vertical layout
+ val leftRect = Rect(
+ paddingLeft, paddingTop,
+ it.left, paddingTop + paddedHeight
+ )
+ val rightRect = Rect(
+ it.right, paddingTop,
+ paddingLeft + paddedWidth, paddingTop + paddedHeight
+ )
- if (feature.bounds.left == 0) { // Horizontal layout
- val topRect = Rect(
- paddingLeft, paddingTop,
- paddingLeft + paddedWidth, splitRect.top
- )
- val bottomRect = Rect(
- paddingLeft, splitRect.bottom,
- paddingLeft + paddedWidth, paddingTop + paddedHeight
- )
-
- if (measureAndCheckMinSize(topRect, startView) &&
- measureAndCheckMinSize(bottomRect, endView)
- ) {
- return arrayOf(topRect, bottomRect)
- }
- } else if (feature.bounds.top == 0) { // Vertical layout
- val leftRect = Rect(
- paddingLeft, paddingTop,
- splitRect.left, paddingTop + paddedHeight
- )
- val rightRect = Rect(
- splitRect.right, paddingTop,
- paddingLeft + paddedWidth, paddingTop + paddedHeight
- )
-
- if (measureAndCheckMinSize(leftRect, startView) &&
- measureAndCheckMinSize(rightRect, endView)
- ) {
- return arrayOf(leftRect, rightRect)
+ if (measureAndCheckMinSize(leftRect, startView) &&
+ measureAndCheckMinSize(rightRect, endView)
+ ) {
+ return arrayOf(leftRect, rightRect)
+ }
+ }
}
}
- }
// We have tried to fit the children and measured them previously. Since they didn't fit,
// we need to measure again to update the stored values.
@@ -191,4 +192,10 @@
return childView.measuredWidthAndState and MEASURED_STATE_TOO_SMALL == 0 &&
childView.measuredHeightAndState and MEASURED_STATE_TOO_SMALL == 0
}
+
+ private fun isValidFoldFeature(displayFeature: DisplayFeature): Boolean {
+ val feature = displayFeature as? FoldingFeature ?: return false
+ return (feature.type == TYPE_FOLD || feature.type == TYPE_HINGE) &&
+ getFeaturePositionInViewRect(feature, this) != null
+ }
}
\ No newline at end of file
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
index 5ee6df2..27754f4 100644
--- 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
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("DEPRECATION") // TODO(b/173739071) Remove DeviceState
+
package androidx.window.sample.backend
import android.app.Activity
@@ -22,7 +24,7 @@
import androidx.core.util.Consumer
import androidx.window.DeviceState
import androidx.window.DisplayFeature
-import androidx.window.DisplayFeature.TYPE_FOLD
+import androidx.window.FoldingFeature
import androidx.window.WindowBackend
import androidx.window.WindowLayoutInfo
import java.util.concurrent.Executor
@@ -53,18 +55,16 @@
SHORT_DIMENSION
}
- private fun getDeviceState(): DeviceState {
- return DeviceState.Builder().setPosture(DeviceState.POSTURE_OPENED).build()
- }
-
private fun getWindowLayoutInfo(activity: Activity): WindowLayoutInfo {
val windowSize = activity.calculateWindowSizeExt()
val featureRect = foldRect(windowSize)
- val displayFeature = DisplayFeature.Builder()
- .setBounds(featureRect)
- .setType(TYPE_FOLD)
- .build()
+ val displayFeature =
+ FoldingFeature(
+ featureRect,
+ FoldingFeature.TYPE_FOLD,
+ FoldingFeature.STATE_FLAT
+ )
val featureList = ArrayList<DisplayFeature>()
featureList.add(displayFeature)
return WindowLayoutInfo.Builder().setDisplayFeatures(featureList).build()
@@ -96,9 +96,7 @@
override fun registerDeviceStateChangeCallback(
executor: Executor,
callback: Consumer<DeviceState>
- ) {
- executor.execute { callback.accept(getDeviceState()) }
- }
+ ) {}
override fun unregisterDeviceStateChangeCallback(callback: Consumer<DeviceState>) {
}
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index 3abcf8b..3055076 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -1,35 +1,37 @@
// Signature format: 4.0
package androidx.window {
- public final class DeviceState {
- method public int getPosture();
- field public static final int POSTURE_CLOSED = 1; // 0x1
- field public static final int POSTURE_FLIPPED = 4; // 0x4
- field public static final int POSTURE_HALF_OPENED = 2; // 0x2
- field public static final int POSTURE_OPENED = 3; // 0x3
- field public static final int POSTURE_UNKNOWN = 0; // 0x0
+ @Deprecated public final class DeviceState {
+ method @Deprecated public int getPosture();
+ field @Deprecated public static final int POSTURE_CLOSED = 1; // 0x1
+ field @Deprecated public static final int POSTURE_FLIPPED = 4; // 0x4
+ field @Deprecated public static final int POSTURE_HALF_OPENED = 2; // 0x2
+ field @Deprecated public static final int POSTURE_OPENED = 3; // 0x3
+ field @Deprecated public static final int POSTURE_UNKNOWN = 0; // 0x0
}
- public static final class DeviceState.Builder {
- ctor public DeviceState.Builder();
- method public androidx.window.DeviceState build();
- method public androidx.window.DeviceState.Builder setPosture(int);
+ @Deprecated public static final class DeviceState.Builder {
+ ctor @Deprecated public DeviceState.Builder();
+ method @Deprecated public androidx.window.DeviceState build();
+ method @Deprecated public androidx.window.DeviceState.Builder setPosture(int);
}
- public final class DisplayFeature {
+ public interface DisplayFeature {
method public android.graphics.Rect getBounds();
+ }
+
+ public class FoldingFeature implements androidx.window.DisplayFeature {
+ ctor public FoldingFeature(android.graphics.Rect, int, int);
+ method public android.graphics.Rect getBounds();
+ method public int getState();
method public int getType();
+ field public static final int STATE_FLAT = 1; // 0x1
+ field public static final int STATE_FLIPPED = 3; // 0x3
+ field public static final int STATE_HALF_OPENED = 2; // 0x2
field public static final int TYPE_FOLD = 1; // 0x1
field public static final int TYPE_HINGE = 2; // 0x2
}
- public static final class DisplayFeature.Builder {
- ctor public DisplayFeature.Builder();
- method public androidx.window.DisplayFeature build();
- method public androidx.window.DisplayFeature.Builder setBounds(android.graphics.Rect);
- method public androidx.window.DisplayFeature.Builder setType(int);
- }
-
public interface WindowBackend {
method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
method public void registerLayoutChangeCallback(android.app.Activity, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
@@ -52,9 +54,9 @@
ctor public WindowManager(android.content.Context, androidx.window.WindowBackend);
method public androidx.window.WindowMetrics getCurrentWindowMetrics();
method public androidx.window.WindowMetrics getMaximumWindowMetrics();
- method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
+ method @Deprecated public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
method public void registerLayoutChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
- method public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
+ method @Deprecated public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
}
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index 3abcf8b..3055076 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -1,35 +1,37 @@
// Signature format: 4.0
package androidx.window {
- public final class DeviceState {
- method public int getPosture();
- field public static final int POSTURE_CLOSED = 1; // 0x1
- field public static final int POSTURE_FLIPPED = 4; // 0x4
- field public static final int POSTURE_HALF_OPENED = 2; // 0x2
- field public static final int POSTURE_OPENED = 3; // 0x3
- field public static final int POSTURE_UNKNOWN = 0; // 0x0
+ @Deprecated public final class DeviceState {
+ method @Deprecated public int getPosture();
+ field @Deprecated public static final int POSTURE_CLOSED = 1; // 0x1
+ field @Deprecated public static final int POSTURE_FLIPPED = 4; // 0x4
+ field @Deprecated public static final int POSTURE_HALF_OPENED = 2; // 0x2
+ field @Deprecated public static final int POSTURE_OPENED = 3; // 0x3
+ field @Deprecated public static final int POSTURE_UNKNOWN = 0; // 0x0
}
- public static final class DeviceState.Builder {
- ctor public DeviceState.Builder();
- method public androidx.window.DeviceState build();
- method public androidx.window.DeviceState.Builder setPosture(int);
+ @Deprecated public static final class DeviceState.Builder {
+ ctor @Deprecated public DeviceState.Builder();
+ method @Deprecated public androidx.window.DeviceState build();
+ method @Deprecated public androidx.window.DeviceState.Builder setPosture(int);
}
- public final class DisplayFeature {
+ public interface DisplayFeature {
method public android.graphics.Rect getBounds();
+ }
+
+ public class FoldingFeature implements androidx.window.DisplayFeature {
+ ctor public FoldingFeature(android.graphics.Rect, int, int);
+ method public android.graphics.Rect getBounds();
+ method public int getState();
method public int getType();
+ field public static final int STATE_FLAT = 1; // 0x1
+ field public static final int STATE_FLIPPED = 3; // 0x3
+ field public static final int STATE_HALF_OPENED = 2; // 0x2
field public static final int TYPE_FOLD = 1; // 0x1
field public static final int TYPE_HINGE = 2; // 0x2
}
- public static final class DisplayFeature.Builder {
- ctor public DisplayFeature.Builder();
- method public androidx.window.DisplayFeature build();
- method public androidx.window.DisplayFeature.Builder setBounds(android.graphics.Rect);
- method public androidx.window.DisplayFeature.Builder setType(int);
- }
-
public interface WindowBackend {
method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
method public void registerLayoutChangeCallback(android.app.Activity, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
@@ -52,9 +54,9 @@
ctor public WindowManager(android.content.Context, androidx.window.WindowBackend);
method public androidx.window.WindowMetrics getCurrentWindowMetrics();
method public androidx.window.WindowMetrics getMaximumWindowMetrics();
- method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
+ method @Deprecated public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
method public void registerLayoutChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
- method public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
+ method @Deprecated public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
}
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index 3abcf8b..3055076 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -1,35 +1,37 @@
// Signature format: 4.0
package androidx.window {
- public final class DeviceState {
- method public int getPosture();
- field public static final int POSTURE_CLOSED = 1; // 0x1
- field public static final int POSTURE_FLIPPED = 4; // 0x4
- field public static final int POSTURE_HALF_OPENED = 2; // 0x2
- field public static final int POSTURE_OPENED = 3; // 0x3
- field public static final int POSTURE_UNKNOWN = 0; // 0x0
+ @Deprecated public final class DeviceState {
+ method @Deprecated public int getPosture();
+ field @Deprecated public static final int POSTURE_CLOSED = 1; // 0x1
+ field @Deprecated public static final int POSTURE_FLIPPED = 4; // 0x4
+ field @Deprecated public static final int POSTURE_HALF_OPENED = 2; // 0x2
+ field @Deprecated public static final int POSTURE_OPENED = 3; // 0x3
+ field @Deprecated public static final int POSTURE_UNKNOWN = 0; // 0x0
}
- public static final class DeviceState.Builder {
- ctor public DeviceState.Builder();
- method public androidx.window.DeviceState build();
- method public androidx.window.DeviceState.Builder setPosture(int);
+ @Deprecated public static final class DeviceState.Builder {
+ ctor @Deprecated public DeviceState.Builder();
+ method @Deprecated public androidx.window.DeviceState build();
+ method @Deprecated public androidx.window.DeviceState.Builder setPosture(int);
}
- public final class DisplayFeature {
+ public interface DisplayFeature {
method public android.graphics.Rect getBounds();
+ }
+
+ public class FoldingFeature implements androidx.window.DisplayFeature {
+ ctor public FoldingFeature(android.graphics.Rect, int, int);
+ method public android.graphics.Rect getBounds();
+ method public int getState();
method public int getType();
+ field public static final int STATE_FLAT = 1; // 0x1
+ field public static final int STATE_FLIPPED = 3; // 0x3
+ field public static final int STATE_HALF_OPENED = 2; // 0x2
field public static final int TYPE_FOLD = 1; // 0x1
field public static final int TYPE_HINGE = 2; // 0x2
}
- public static final class DisplayFeature.Builder {
- ctor public DisplayFeature.Builder();
- method public androidx.window.DisplayFeature build();
- method public androidx.window.DisplayFeature.Builder setBounds(android.graphics.Rect);
- method public androidx.window.DisplayFeature.Builder setType(int);
- }
-
public interface WindowBackend {
method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
method public void registerLayoutChangeCallback(android.app.Activity, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
@@ -52,9 +54,9 @@
ctor public WindowManager(android.content.Context, androidx.window.WindowBackend);
method public androidx.window.WindowMetrics getCurrentWindowMetrics();
method public androidx.window.WindowMetrics getMaximumWindowMetrics();
- method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
+ method @Deprecated public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
method public void registerLayoutChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
- method public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
+ method @Deprecated public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
}
diff --git a/window/window/build.gradle b/window/window/build.gradle
index 30a2fa6..4b98a67 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -18,12 +18,16 @@
import androidx.build.LibraryVersions
import androidx.build.Publish
+import static androidx.build.dependencies.DependenciesKt.ANDROIDX_TEST_CORE
import static androidx.build.dependencies.DependenciesKt.ANDROIDX_TEST_EXT_JUNIT
import static androidx.build.dependencies.DependenciesKt.ANDROIDX_TEST_RULES
import static androidx.build.dependencies.DependenciesKt.ANDROIDX_TEST_RUNNER
import static androidx.build.dependencies.DependenciesKt.DEXMAKER_MOCKITO
+import static androidx.build.dependencies.DependenciesKt.JUNIT
import static androidx.build.dependencies.DependenciesKt.MOCKITO_CORE
+import static androidx.build.dependencies.DependenciesKt.ROBOLECTRIC
import static androidx.build.dependencies.DependenciesKt.TRUTH
+import static androidx.build.dependencies.DependenciesKt.getKOTLIN_STDLIB
plugins {
id("AndroidXPlugin")
@@ -47,6 +51,16 @@
compileOnly(project(":window:window-extensions"))
compileOnly(project(":window:window-sidecar"))
+ testImplementation(KOTLIN_STDLIB)
+ testImplementation(ANDROIDX_TEST_CORE)
+ testImplementation(ANDROIDX_TEST_RUNNER)
+ testImplementation(JUNIT)
+ testImplementation(TRUTH)
+ testImplementation(ROBOLECTRIC)
+ testImplementation(MOCKITO_CORE)
+ testImplementation(compileOnly(project(":window:window-extensions")))
+ testImplementation(compileOnly(project(":window:window-sidecar")))
+
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
androidTestImplementation(ANDROIDX_TEST_RUNNER)
androidTestImplementation(ANDROIDX_TEST_RULES)
diff --git a/window/window/src/androidTest/java/androidx/window/DisplayFeatureTest.java b/window/window/src/androidTest/java/androidx/window/DisplayFeatureTest.java
deleted file mode 100644
index 93033ae..0000000
--- a/window/window/src/androidTest/java/androidx/window/DisplayFeatureTest.java
+++ /dev/null
@@ -1,134 +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;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-
-import android.graphics.Rect;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/** Tests for {@link DisplayFeature} class. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public final class DisplayFeatureTest {
-
- @Test(expected = IllegalArgumentException.class)
- public void testBuilder_empty() {
- new DisplayFeature.Builder().build();
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testBuilder_foldWithNonZeroArea() {
- DisplayFeature feature = new DisplayFeature.Builder()
- .setBounds(new Rect(10, 0, 20, 30))
- .setType(DisplayFeature.TYPE_FOLD).build();
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testBuilder_horizontalHingeWithNonZeroOrigin() {
- DisplayFeature horizontalHinge = new DisplayFeature.Builder()
- .setBounds(new Rect(1, 10, 20, 10))
- .setType(DisplayFeature.TYPE_HINGE).build();
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testBuilder_verticalHingeWithNonZeroOrigin() {
- DisplayFeature verticalHinge = new DisplayFeature.Builder()
- .setBounds(new Rect(10, 1, 10, 20))
- .setType(DisplayFeature.TYPE_HINGE).build();
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testBuilder_horizontalFoldWithNonZeroOrigin() {
- DisplayFeature horizontalFold = new DisplayFeature.Builder()
- .setBounds(new Rect(1, 10, 20, 10))
- .setType(DisplayFeature.TYPE_FOLD).build();
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testBuilder_verticalFoldWithNonZeroOrigin() {
- DisplayFeature verticalFold = new DisplayFeature.Builder()
- .setBounds(new Rect(10, 1, 10, 20))
- .setType(DisplayFeature.TYPE_FOLD).build();
- }
-
- @Test
- public void testBuilder_setBoundsAndType() {
- DisplayFeature.Builder builder = new DisplayFeature.Builder();
- Rect bounds = new Rect(0, 10, 30, 10);
- builder.setBounds(bounds);
- builder.setType(DisplayFeature.TYPE_HINGE);
- DisplayFeature feature = builder.build();
-
- assertEquals(bounds, feature.getBounds());
- assertEquals(DisplayFeature.TYPE_HINGE, feature.getType());
- }
-
- @Test
- public void testEquals_sameAttributes() {
- Rect bounds = new Rect(1, 0, 1, 10);
- int type = DisplayFeature.TYPE_FOLD;
-
- DisplayFeature original = new DisplayFeature(bounds, type);
- DisplayFeature copy = new DisplayFeature(bounds, type);
-
- assertEquals(original, copy);
- }
-
- @Test
- public void testEquals_differentRect() {
- Rect originalRect = new Rect(1, 0, 1, 10);
- Rect otherRect = new Rect(2, 0, 2, 10);
- int type = DisplayFeature.TYPE_FOLD;
-
- DisplayFeature original = new DisplayFeature(originalRect, type);
- DisplayFeature other = new DisplayFeature(otherRect, type);
-
- assertNotEquals(original, other);
- }
-
- @Test
- public void testEquals_differentType() {
- Rect rect = new Rect(1, 0, 1, 10);
- int originalType = DisplayFeature.TYPE_FOLD;
- int otherType = DisplayFeature.TYPE_HINGE;
-
- DisplayFeature original = new DisplayFeature(rect, originalType);
- DisplayFeature other = new DisplayFeature(rect, otherType);
-
- assertNotEquals(original, other);
- }
-
- @Test
- public void testHashCode_matchesIfEqual() {
- Rect originalRect = new Rect(1, 0, 1, 10);
- Rect matchingRect = new Rect(1, 0, 1, 10);
- int type = DisplayFeature.TYPE_FOLD;
-
- DisplayFeature original = new DisplayFeature(originalRect, type);
- DisplayFeature matching = new DisplayFeature(matchingRect, type);
-
- assertEquals(original, matching);
- assertEquals(original.hashCode(), matching.hashCode());
- }
-}
diff --git a/window/window/src/androidTest/java/androidx/window/ExtensionAdapterTest.java b/window/window/src/androidTest/java/androidx/window/ExtensionAdapterTest.java
index c6e6348..9932500 100644
--- a/window/window/src/androidTest/java/androidx/window/ExtensionAdapterTest.java
+++ b/window/window/src/androidTest/java/androidx/window/ExtensionAdapterTest.java
@@ -25,6 +25,7 @@
import androidx.window.extensions.ExtensionDeviceState;
import androidx.window.extensions.ExtensionDisplayFeature;
+import androidx.window.extensions.ExtensionFoldingFeature;
import androidx.window.extensions.ExtensionWindowLayoutInfo;
import org.junit.After;
@@ -64,8 +65,8 @@
public void testTranslate_validFeature() {
Activity mockActivity = mock(Activity.class);
Rect bounds = new Rect(WINDOW_BOUNDS.left, 0, WINDOW_BOUNDS.right, 0);
- ExtensionDisplayFeature foldFeature = new ExtensionDisplayFeature(bounds,
- ExtensionDisplayFeature.TYPE_FOLD);
+ ExtensionDisplayFeature foldFeature = new ExtensionFoldingFeature(bounds,
+ ExtensionFoldingFeature.TYPE_FOLD, ExtensionFoldingFeature.STATE_FLAT);
List<ExtensionDisplayFeature> extensionDisplayFeatures = new ArrayList<>();
extensionDisplayFeatures.add(foldFeature);
@@ -73,7 +74,8 @@
new ExtensionWindowLayoutInfo(extensionDisplayFeatures);
List<DisplayFeature> expectedFeatures = new ArrayList<>();
- expectedFeatures.add(new DisplayFeature(foldFeature.getBounds(), DisplayFeature.TYPE_FOLD));
+ expectedFeatures.add(new FoldingFeature(foldFeature.getBounds(), FoldingFeature.TYPE_FOLD,
+ FoldingFeature.STATE_FLAT));
WindowLayoutInfo expected = new WindowLayoutInfo(expectedFeatures);
ExtensionAdapter adapter = new ExtensionAdapter();
@@ -85,59 +87,17 @@
@Test
@Override
- public void testTranslateWindowLayoutInfo_filterRemovesEmptyBoundsFeature() {
- List<ExtensionDisplayFeature> extensionDisplayFeatures = new ArrayList<>();
- extensionDisplayFeatures.add(
- new ExtensionDisplayFeature(new Rect(), ExtensionDisplayFeature.TYPE_FOLD));
-
- ExtensionAdapter adapter = new ExtensionAdapter();
- ExtensionWindowLayoutInfo windowLayoutInfo =
- new ExtensionWindowLayoutInfo(extensionDisplayFeatures);
- Activity mockActivity = mock(Activity.class);
-
- WindowLayoutInfo actual = adapter.translate(mockActivity, windowLayoutInfo);
-
- assertTrue("Remove empty bounds feature", actual.getDisplayFeatures().isEmpty());
- }
-
-
- @Test
- @Override
- public void testTranslateWindowLayoutInfo_filterRemovesNonEmptyAreaFoldFeature() {
- List<ExtensionDisplayFeature> extensionDisplayFeatures = new ArrayList<>();
- Rect fullWidthBounds = new Rect(0, 1, WINDOW_BOUNDS.width(), 2);
- Rect fullHeightBounds = new Rect(1, 0, 2, WINDOW_BOUNDS.height());
- extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullWidthBounds,
- ExtensionDisplayFeature.TYPE_FOLD));
- extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullHeightBounds,
- ExtensionDisplayFeature.TYPE_FOLD));
-
- ExtensionAdapter extensionCallbackAdapter = new ExtensionAdapter();
- ExtensionWindowLayoutInfo windowLayoutInfo =
- new ExtensionWindowLayoutInfo(extensionDisplayFeatures);
- Activity mockActivity = mock(Activity.class);
-
- WindowLayoutInfo actual = extensionCallbackAdapter.translate(mockActivity,
- windowLayoutInfo);
-
- assertTrue("Remove non empty area fold feature", actual.getDisplayFeatures().isEmpty());
- }
-
- @Test
- @Override
public void testTranslateWindowLayoutInfo_filterRemovesHingeFeatureNotSpanningFullDimension() {
List<ExtensionDisplayFeature> extensionDisplayFeatures = new ArrayList<>();
Rect fullWidthBounds = new Rect(WINDOW_BOUNDS.left, WINDOW_BOUNDS.top,
WINDOW_BOUNDS.right / 2, 2);
Rect fullHeightBounds = new Rect(WINDOW_BOUNDS.left, WINDOW_BOUNDS.top, 2,
WINDOW_BOUNDS.bottom / 2);
- extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullWidthBounds,
- ExtensionDisplayFeature.TYPE_HINGE));
- extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullHeightBounds,
- ExtensionDisplayFeature.TYPE_HINGE));
+ extensionDisplayFeatures.add(new ExtensionFoldingFeature(fullWidthBounds,
+ ExtensionFoldingFeature.TYPE_HINGE, ExtensionFoldingFeature.STATE_FLAT));
+ extensionDisplayFeatures.add(new ExtensionFoldingFeature(fullHeightBounds,
+ ExtensionFoldingFeature.TYPE_HINGE, ExtensionFoldingFeature.STATE_FLAT));
- ExtensionInterfaceCompat.ExtensionCallbackInterface mockCallback = mock(
- ExtensionInterfaceCompat.ExtensionCallbackInterface.class);
ExtensionAdapter extensionCallbackAdapter = new ExtensionAdapter();
ExtensionWindowLayoutInfo windowLayoutInfo =
new ExtensionWindowLayoutInfo(extensionDisplayFeatures);
@@ -159,10 +119,10 @@
WINDOW_BOUNDS.right / 2, WINDOW_BOUNDS.top);
Rect fullHeightBounds = new Rect(WINDOW_BOUNDS.left, WINDOW_BOUNDS.top, WINDOW_BOUNDS.left,
WINDOW_BOUNDS.bottom / 2);
- extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullWidthBounds,
- ExtensionDisplayFeature.TYPE_HINGE));
- extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullHeightBounds,
- ExtensionDisplayFeature.TYPE_HINGE));
+ extensionDisplayFeatures.add(new ExtensionFoldingFeature(fullWidthBounds,
+ ExtensionFoldingFeature.TYPE_HINGE, ExtensionFoldingFeature.STATE_FLAT));
+ extensionDisplayFeatures.add(new ExtensionFoldingFeature(fullHeightBounds,
+ ExtensionFoldingFeature.TYPE_HINGE, ExtensionFoldingFeature.STATE_FLAT));
ExtensionAdapter adapter = new ExtensionAdapter();
ExtensionWindowLayoutInfo windowLayoutInfo =
@@ -183,20 +143,14 @@
List<DeviceState> values = new ArrayList<>();
values.add(extensionCallbackAdapter.translate(new ExtensionDeviceState(
- ExtensionDeviceState.POSTURE_UNKNOWN)));
- values.add(extensionCallbackAdapter.translate(new ExtensionDeviceState(
- ExtensionDeviceState.POSTURE_CLOSED)));
- values.add(extensionCallbackAdapter.translate(new ExtensionDeviceState(
ExtensionDeviceState.POSTURE_HALF_OPENED)));
values.add(extensionCallbackAdapter.translate(new ExtensionDeviceState(
ExtensionDeviceState.POSTURE_OPENED)));
values.add(extensionCallbackAdapter.translate(new ExtensionDeviceState(
ExtensionDeviceState.POSTURE_FLIPPED)));
- assertEquals(DeviceState.POSTURE_UNKNOWN, values.get(0).getPosture());
- assertEquals(DeviceState.POSTURE_CLOSED, values.get(1).getPosture());
- assertEquals(DeviceState.POSTURE_HALF_OPENED, values.get(2).getPosture());
- assertEquals(DeviceState.POSTURE_OPENED, values.get(3).getPosture());
- assertEquals(DeviceState.POSTURE_FLIPPED, values.get(4).getPosture());
+ assertEquals(DeviceState.POSTURE_HALF_OPENED, values.get(0).getPosture());
+ assertEquals(DeviceState.POSTURE_OPENED, values.get(1).getPosture());
+ assertEquals(DeviceState.POSTURE_FLIPPED, values.get(2).getPosture());
}
}
diff --git a/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java b/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java
index f2d1f92..c0d318e 100644
--- a/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java
+++ b/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java
@@ -17,11 +17,12 @@
package androidx.window;
import static androidx.window.ExtensionInterfaceCompat.ExtensionCallbackInterface;
-import static androidx.window.TestBoundUtil.invalidFoldBounds;
-import static androidx.window.TestBoundUtil.invalidHingeBounds;
-import static androidx.window.TestBoundUtil.validFoldBound;
+import static androidx.window.TestBoundsUtil.invalidFoldBounds;
+import static androidx.window.TestBoundsUtil.invalidHingeBounds;
+import static androidx.window.TestBoundsUtil.validFoldBound;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
@@ -37,6 +38,7 @@
import androidx.test.filters.LargeTest;
import androidx.window.extensions.ExtensionDeviceState;
import androidx.window.extensions.ExtensionDisplayFeature;
+import androidx.window.extensions.ExtensionFoldingFeature;
import androidx.window.extensions.ExtensionInterface;
import androidx.window.extensions.ExtensionWindowLayoutInfo;
@@ -62,19 +64,17 @@
private static final Rect WINDOW_BOUNDS = new Rect(0, 0, 50, 100);
ExtensionCompat mExtensionCompat;
- private ExtensionInterface mMockExtensionInterface;
private Activity mActivity;
@Before
public void setUp() {
- mMockExtensionInterface = mock(ExtensionInterface.class);
- mExtensionCompat = new ExtensionCompat(mMockExtensionInterface, new ExtensionAdapter());
+ mExtensionCompat = new ExtensionCompat(mock(ExtensionInterface.class),
+ new ExtensionAdapter());
mActivity = mock(Activity.class);
TestWindowBoundsHelper mWindowBoundsHelper = new TestWindowBoundsHelper();
mWindowBoundsHelper.setCurrentBounds(WINDOW_BOUNDS);
WindowBoundsHelper.setForTesting(mWindowBoundsHelper);
-
}
@After
@@ -141,7 +141,8 @@
mExtensionCompat.onWindowLayoutChangeListenerAdded(mActivity);
Rect bounds = new Rect(WINDOW_BOUNDS.left, WINDOW_BOUNDS.top, WINDOW_BOUNDS.width(), 1);
ExtensionDisplayFeature extensionDisplayFeature =
- new ExtensionDisplayFeature(bounds, ExtensionDisplayFeature.TYPE_HINGE);
+ new ExtensionFoldingFeature(bounds, ExtensionFoldingFeature.TYPE_HINGE,
+ ExtensionFoldingFeature.STATE_FLIPPED);
List<ExtensionDisplayFeature> displayFeatures = new ArrayList<>();
displayFeatures.add(extensionDisplayFeature);
ExtensionWindowLayoutInfo extensionWindowLayoutInfo =
@@ -156,7 +157,10 @@
WindowLayoutInfo capturedLayout = windowLayoutInfoCaptor.getValue();
assertEquals(1, capturedLayout.getDisplayFeatures().size());
DisplayFeature capturedDisplayFeature = capturedLayout.getDisplayFeatures().get(0);
- assertEquals(DisplayFeature.TYPE_HINGE, capturedDisplayFeature.getType());
+
+ FoldingFeature foldingFeature = (FoldingFeature) capturedDisplayFeature;
+ assertNotNull(foldingFeature);
+ assertEquals(FoldingFeature.TYPE_HINGE, foldingFeature.getType());
assertEquals(bounds, capturedDisplayFeature.getBounds());
}
@@ -263,13 +267,15 @@
List<ExtensionDisplayFeature> malformedFeatures = new ArrayList<>();
for (Rect malformedBound : invalidFoldBounds(WINDOW_BOUNDS)) {
- malformedFeatures.add(new ExtensionDisplayFeature(malformedBound,
- ExtensionDisplayFeature.TYPE_FOLD));
+ malformedFeatures.add(new ExtensionFoldingFeature(malformedBound,
+ ExtensionFoldingFeature.TYPE_FOLD,
+ ExtensionFoldingFeature.STATE_FLAT));
}
for (Rect malformedBound : invalidHingeBounds(WINDOW_BOUNDS)) {
- malformedFeatures.add(new ExtensionDisplayFeature(malformedBound,
- ExtensionDisplayFeature.TYPE_HINGE));
+ malformedFeatures.add(new ExtensionFoldingFeature(malformedBound,
+ ExtensionFoldingFeature.TYPE_HINGE,
+ ExtensionFoldingFeature.STATE_FLAT));
}
return new ExtensionWindowLayoutInfo(malformedFeatures);
@@ -278,8 +284,8 @@
private ExtensionWindowLayoutInfo validWindowLayoutInfo() {
List<ExtensionDisplayFeature> validFeatures = new ArrayList<>();
- validFeatures.add(new ExtensionDisplayFeature(validFoldBound(WINDOW_BOUNDS),
- ExtensionDisplayFeature.TYPE_FOLD));
+ validFeatures.add(new ExtensionFoldingFeature(validFoldBound(WINDOW_BOUNDS),
+ ExtensionFoldingFeature.TYPE_FOLD, ExtensionFoldingFeature.STATE_FLAT));
return new ExtensionWindowLayoutInfo(validFeatures);
}
diff --git a/window/window/src/androidTest/java/androidx/window/ExtensionTest.java b/window/window/src/androidTest/java/androidx/window/ExtensionTest.java
index 3e89816..a34be4a 100644
--- a/window/window/src/androidTest/java/androidx/window/ExtensionTest.java
+++ b/window/window/src/androidTest/java/androidx/window/ExtensionTest.java
@@ -20,8 +20,6 @@
import static androidx.window.ExtensionWindowBackend.initAndVerifyExtension;
import static androidx.window.Version.VERSION_0_1;
import static androidx.window.Version.VERSION_1_0;
-import static androidx.window.extensions.ExtensionDisplayFeature.TYPE_FOLD;
-import static androidx.window.extensions.ExtensionDisplayFeature.TYPE_HINGE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -35,6 +33,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
import android.graphics.Rect;
import androidx.test.core.app.ApplicationProvider;
@@ -42,7 +41,7 @@
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.window.extensions.ExtensionDeviceState;
-import androidx.window.extensions.ExtensionDisplayFeature;
+import androidx.window.extensions.ExtensionFoldingFeature;
import org.junit.Before;
import org.junit.Test;
@@ -79,8 +78,6 @@
public void testDeviceStateCallback() {
assumeExtensionV10_V01();
final Set<Integer> validValues = new HashSet<>();
- validValues.add(ExtensionDeviceState.POSTURE_UNKNOWN);
- validValues.add(ExtensionDeviceState.POSTURE_CLOSED);
validValues.add(ExtensionDeviceState.POSTURE_FLIPPED);
validValues.add(ExtensionDeviceState.POSTURE_HALF_OPENED);
validValues.add(ExtensionDeviceState.POSTURE_OPENED);
@@ -89,8 +86,7 @@
extension.setExtensionCallback(callbackInterface);
extension.onDeviceStateListenersChanged(false);
- verify(callbackInterface).onDeviceStateChanged(argThat(
- deviceState -> validValues.contains(deviceState.getPosture())));
+ verify(callbackInterface, atLeastOnce()).onDeviceStateChanged(any());
}
@Test
@@ -106,15 +102,16 @@
public void testDisplayFeatureDataClass() {
assumeExtensionV10_V01();
- Rect rect = new Rect(1, 2, 3, 4);
+ Rect rect = new Rect(0, 100, 100, 100);
int type = 1;
- ExtensionDisplayFeature displayFeature = new ExtensionDisplayFeature(rect, type);
+ int state = 1;
+ ExtensionFoldingFeature displayFeature =
+ new ExtensionFoldingFeature(rect, type, state);
assertEquals(rect, displayFeature.getBounds());
- assertEquals(type, displayFeature.getType());
}
@Test
- public void testWindowLayoutInfoCallback() {
+ public void testWindowLayoutCallback() {
assumeExtensionV10_V01();
ExtensionInterfaceCompat extension = initAndVerifyExtension(mContext);
ExtensionCallbackInterface callbackInterface = mock(ExtensionCallbackInterface.class);
@@ -125,7 +122,7 @@
assertTrue("Layout must happen after launch", activity.waitForLayout());
- verify(callbackInterface).onWindowLayoutChanged(any(), argThat(
+ verify(callbackInterface, atLeastOnce()).onWindowLayoutChanged(any(), argThat(
new WindowLayoutInfoValidator(activity)));
}
@@ -155,14 +152,30 @@
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
activity.waitForLayout();
+ if (activity.getResources().getConfiguration().orientation
+ != Configuration.ORIENTATION_PORTRAIT) {
+ // Orientation change did not occur on this device config. Skipping the test.
+ return;
+ }
activity.resetLayoutCounter();
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
- assertTrue("Layout must happen after orientation change", activity.waitForLayout());
+ boolean layoutHappened = activity.waitForLayout();
+ if (activity.getResources().getConfiguration().orientation
+ != Configuration.ORIENTATION_LANDSCAPE) {
+ // Orientation change did not occur on this device config. Skipping the test.
+ return;
+ }
+ assertTrue("Layout must happen after orientation change", layoutHappened);
- verify(callbackInterface, atLeastOnce())
- .onWindowLayoutChanged(any(), argThat(new DistinctWindowLayoutInfoMatcher()));
+ if (!isSidecar()) {
+ verify(callbackInterface, atLeastOnce())
+ .onWindowLayoutChanged(any(), argThat(new DistinctWindowLayoutInfoMatcher()));
+ } else {
+ verify(callbackInterface, atLeastOnce())
+ .onWindowLayoutChanged(any(), any());
+ }
}
@Test
@@ -181,6 +194,11 @@
activity = mActivityTestRule.getActivity();
activity.waitForLayout();
+ if (activity.getResources().getConfiguration().orientation
+ != Configuration.ORIENTATION_PORTRAIT) {
+ // Orientation change did not occur on this device config. Skipping the test.
+ return;
+ }
TestActivity.resetResumeCounter();
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
@@ -189,9 +207,19 @@
activity = mActivityTestRule.getActivity();
activity.waitForLayout();
+ if (activity.getResources().getConfiguration().orientation
+ != Configuration.ORIENTATION_LANDSCAPE) {
+ // Orientation change did not occur on this device config. Skipping the test.
+ return;
+ }
- verify(callbackInterface, atLeastOnce())
- .onWindowLayoutChanged(any(), argThat(new DistinctWindowLayoutInfoMatcher()));
+ if (!isSidecar()) {
+ verify(callbackInterface, atLeastOnce())
+ .onWindowLayoutChanged(any(), argThat(new DistinctWindowLayoutInfoMatcher()));
+ } else {
+ verify(callbackInterface, atLeastOnce())
+ .onWindowLayoutChanged(any(), any());
+ }
}
@Test
@@ -212,6 +240,10 @@
|| VERSION_0_1.equals(SidecarCompat.getSidecarVersion()));
}
+ private boolean isSidecar() {
+ return SidecarCompat.getSidecarVersion() != null;
+ }
+
/**
* An argument matcher that ensures the arguments used to call are distinct. The only exception
* is to allow the first value to trigger twice in case the initial value is pushed and then
@@ -253,26 +285,38 @@
return true;
}
- for (DisplayFeature displayFeature :
- windowLayoutInfo.getDisplayFeatures()) {
- int featureType = displayFeature.getType();
- if (featureType != TYPE_FOLD && featureType != TYPE_HINGE) {
- return false;
- }
-
- Rect featureRect = displayFeature.getBounds();
-
- if (featureRect.isEmpty() || featureRect.left < 0 || featureRect.top < 0) {
- return false;
- }
- if (featureRect.right < 1 || featureRect.right > mActivity.getWidth()) {
- return false;
- }
- if (featureRect.bottom < 1 || featureRect.bottom > mActivity.getHeight()) {
+ for (DisplayFeature displayFeature : windowLayoutInfo.getDisplayFeatures()) {
+ if (!isValid(mActivity, displayFeature)) {
return false;
}
}
return true;
}
}
+
+ private static boolean isValid(TestActivity activity, DisplayFeature displayFeature) {
+ if (!(displayFeature instanceof FoldingFeature)) {
+ return false;
+ }
+ FoldingFeature feature = (FoldingFeature) displayFeature;
+ int featureType = feature.getType();
+ if (featureType != FoldingFeature.TYPE_FOLD && featureType != FoldingFeature.TYPE_HINGE) {
+ return false;
+ }
+
+ Rect featureRect = feature.getBounds();
+ WindowMetrics windowMetrics = new WindowManager(activity).getCurrentWindowMetrics();
+
+ if ((featureRect.height() == 0 && featureRect.width() == 0) || featureRect.left < 0
+ || featureRect.top < 0) {
+ return false;
+ }
+ if (featureRect.right < 1 || featureRect.right > windowMetrics.getBounds().width()) {
+ return false;
+ }
+ if (featureRect.bottom < 1 || featureRect.bottom > windowMetrics.getBounds().height()) {
+ return false;
+ }
+ return true;
+ }
}
diff --git a/window/window/src/androidTest/java/androidx/window/ExtensionTranslatingCallbackTest.java b/window/window/src/androidTest/java/androidx/window/ExtensionTranslatingCallbackTest.java
index 09a9305..fee07ed 100644
--- a/window/window/src/androidTest/java/androidx/window/ExtensionTranslatingCallbackTest.java
+++ b/window/window/src/androidTest/java/androidx/window/ExtensionTranslatingCallbackTest.java
@@ -30,6 +30,7 @@
import androidx.window.extensions.ExtensionDeviceState;
import androidx.window.extensions.ExtensionDisplayFeature;
+import androidx.window.extensions.ExtensionFoldingFeature;
import androidx.window.extensions.ExtensionWindowLayoutInfo;
import org.junit.After;
@@ -62,8 +63,8 @@
public void testOnWindowLayoutChange_validFeature() {
Activity mockActivity = mock(Activity.class);
Rect bounds = new Rect(WINDOW_BOUNDS.left, 0, WINDOW_BOUNDS.right, 0);
- ExtensionDisplayFeature foldFeature = new ExtensionDisplayFeature(bounds,
- ExtensionDisplayFeature.TYPE_FOLD);
+ ExtensionDisplayFeature foldFeature = new ExtensionFoldingFeature(bounds,
+ ExtensionFoldingFeature.TYPE_FOLD, ExtensionFoldingFeature.STATE_FLAT);
List<ExtensionDisplayFeature> extensionDisplayFeatures = new ArrayList<>();
extensionDisplayFeatures.add(foldFeature);
@@ -71,7 +72,8 @@
new ExtensionWindowLayoutInfo(extensionDisplayFeatures);
List<DisplayFeature> expectedFeatures = new ArrayList<>();
- expectedFeatures.add(new DisplayFeature(foldFeature.getBounds(), DisplayFeature.TYPE_FOLD));
+ expectedFeatures.add(new FoldingFeature(foldFeature.getBounds(), FoldingFeature.TYPE_FOLD,
+ FoldingFeature.STATE_FLAT));
WindowLayoutInfo expected = new WindowLayoutInfo(expectedFeatures);
ExtensionCallbackInterface mockCallback = mock(ExtensionCallbackInterface.class);
@@ -86,59 +88,16 @@
}
@Test
- public void testOnWindowLayoutChange_filterRemovesEmptyBoundsFeature() {
- List<ExtensionDisplayFeature> extensionDisplayFeatures = new ArrayList<>();
- extensionDisplayFeatures.add(
- new ExtensionDisplayFeature(new Rect(), ExtensionDisplayFeature.TYPE_FOLD));
-
- ExtensionCallbackInterface mockCallback = mock(ExtensionCallbackInterface.class);
- ExtensionTranslatingCallback extensionTranslatingCallback =
- new ExtensionTranslatingCallback(mockCallback, new ExtensionAdapter());
- ExtensionWindowLayoutInfo windowLayoutInfo =
- new ExtensionWindowLayoutInfo(extensionDisplayFeatures);
- Activity mockActivity = mock(Activity.class);
-
- extensionTranslatingCallback.onWindowLayoutChanged(mockActivity, windowLayoutInfo);
-
- verify(mockCallback).onWindowLayoutChanged(eq(mockActivity),
- argThat((layoutInfo) -> layoutInfo.getDisplayFeatures().isEmpty()));
- }
-
-
- @Test
- public void testOnWindowLayoutChange_filterRemovesNonEmptyAreaFoldFeature() {
- List<ExtensionDisplayFeature> extensionDisplayFeatures = new ArrayList<>();
- Rect fullWidthBounds = new Rect(0, 1, WINDOW_BOUNDS.width(), 2);
- Rect fullHeightBounds = new Rect(1, 0, 2, WINDOW_BOUNDS.height());
- extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullWidthBounds,
- ExtensionDisplayFeature.TYPE_FOLD));
- extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullHeightBounds,
- ExtensionDisplayFeature.TYPE_FOLD));
-
- ExtensionCallbackInterface mockCallback = mock(ExtensionCallbackInterface.class);
- ExtensionTranslatingCallback extensionTranslatingCallback =
- new ExtensionTranslatingCallback(mockCallback, new ExtensionAdapter());
- ExtensionWindowLayoutInfo windowLayoutInfo =
- new ExtensionWindowLayoutInfo(extensionDisplayFeatures);
- Activity mockActivity = mock(Activity.class);
-
- extensionTranslatingCallback.onWindowLayoutChanged(mockActivity, windowLayoutInfo);
-
- verify(mockCallback).onWindowLayoutChanged(eq(mockActivity),
- argThat((layoutInfo) -> layoutInfo.getDisplayFeatures().isEmpty()));
- }
-
- @Test
public void testOnWindowLayoutChange_filterRemovesHingeFeatureNotSpanningFullDimension() {
List<ExtensionDisplayFeature> extensionDisplayFeatures = new ArrayList<>();
Rect fullWidthBounds = new Rect(WINDOW_BOUNDS.left, WINDOW_BOUNDS.top,
WINDOW_BOUNDS.right / 2, 2);
Rect fullHeightBounds = new Rect(WINDOW_BOUNDS.left, WINDOW_BOUNDS.top, 2,
WINDOW_BOUNDS.bottom / 2);
- extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullWidthBounds,
- ExtensionDisplayFeature.TYPE_HINGE));
- extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullHeightBounds,
- ExtensionDisplayFeature.TYPE_HINGE));
+ extensionDisplayFeatures.add(new ExtensionFoldingFeature(fullWidthBounds,
+ ExtensionFoldingFeature.TYPE_HINGE, ExtensionFoldingFeature.STATE_FLAT));
+ extensionDisplayFeatures.add(new ExtensionFoldingFeature(fullHeightBounds,
+ ExtensionFoldingFeature.TYPE_HINGE, ExtensionFoldingFeature.STATE_FLAT));
ExtensionCallbackInterface mockCallback = mock(ExtensionCallbackInterface.class);
ExtensionTranslatingCallback extensionTranslatingCallback =
@@ -161,10 +120,10 @@
WINDOW_BOUNDS.right / 2, WINDOW_BOUNDS.top);
Rect fullHeightBounds = new Rect(WINDOW_BOUNDS.left, WINDOW_BOUNDS.top, WINDOW_BOUNDS.left,
WINDOW_BOUNDS.bottom / 2);
- extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullWidthBounds,
- ExtensionDisplayFeature.TYPE_HINGE));
- extensionDisplayFeatures.add(new ExtensionDisplayFeature(fullHeightBounds,
- ExtensionDisplayFeature.TYPE_HINGE));
+ extensionDisplayFeatures.add(new ExtensionFoldingFeature(fullWidthBounds,
+ ExtensionFoldingFeature.TYPE_HINGE, ExtensionFoldingFeature.STATE_FLAT));
+ extensionDisplayFeatures.add(new ExtensionFoldingFeature(fullHeightBounds,
+ ExtensionFoldingFeature.TYPE_HINGE, ExtensionFoldingFeature.STATE_FLAT));
ExtensionCallbackInterface mockCallback = mock(ExtensionCallbackInterface.class);
ExtensionTranslatingCallback extensionTranslatingCallback =
@@ -187,10 +146,6 @@
new ExtensionTranslatingCallback(mockCallback, new ExtensionAdapter());
extensionTranslatingCallback.onDeviceStateChanged(new ExtensionDeviceState(
- ExtensionDeviceState.POSTURE_UNKNOWN));
- extensionTranslatingCallback.onDeviceStateChanged(new ExtensionDeviceState(
- ExtensionDeviceState.POSTURE_CLOSED));
- extensionTranslatingCallback.onDeviceStateChanged(new ExtensionDeviceState(
ExtensionDeviceState.POSTURE_HALF_OPENED));
extensionTranslatingCallback.onDeviceStateChanged(new ExtensionDeviceState(
ExtensionDeviceState.POSTURE_OPENED));
@@ -201,11 +156,9 @@
verify(mockCallback, atLeastOnce()).onDeviceStateChanged(captor.capture());
List<DeviceState> values = captor.getAllValues();
- assertEquals(DeviceState.POSTURE_UNKNOWN, values.get(0).getPosture());
- assertEquals(DeviceState.POSTURE_CLOSED, values.get(1).getPosture());
- assertEquals(DeviceState.POSTURE_HALF_OPENED, values.get(2).getPosture());
- assertEquals(DeviceState.POSTURE_OPENED, values.get(3).getPosture());
- assertEquals(DeviceState.POSTURE_FLIPPED, values.get(4).getPosture());
- assertEquals(5, values.size());
+ assertEquals(DeviceState.POSTURE_HALF_OPENED, values.get(0).getPosture());
+ assertEquals(DeviceState.POSTURE_OPENED, values.get(1).getPosture());
+ assertEquals(DeviceState.POSTURE_FLIPPED, values.get(2).getPosture());
+ assertEquals(3, values.size());
}
}
diff --git a/window/window/src/androidTest/java/androidx/window/ExtensionWindowBackendTest.java b/window/window/src/androidTest/java/androidx/window/ExtensionWindowBackendTest.java
index 2361ed1..97fbd20 100644
--- a/window/window/src/androidTest/java/androidx/window/ExtensionWindowBackendTest.java
+++ b/window/window/src/androidTest/java/androidx/window/ExtensionWindowBackendTest.java
@@ -52,6 +52,7 @@
import java.util.List;
/** Tests for {@link ExtensionWindowBackend} class. */
+@SuppressWarnings("deprecation") // TODO(b/173739071) remove DeviceState
@LargeTest
@RunWith(AndroidJUnit4.class)
public final class ExtensionWindowBackendTest extends WindowTestBase {
@@ -132,7 +133,7 @@
backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
// Check registering the layout change callback
- Consumer<WindowLayoutInfo> consumer = mock(Consumer.class);
+ Consumer<WindowLayoutInfo> consumer = mock(WindowLayoutInfoConsumer.class);
TestActivity activity = mActivityTestRule.launchActivity(new Intent());
backend.registerLayoutChangeCallback(activity, Runnable::run, consumer);
@@ -152,10 +153,11 @@
backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
// Check registering the layout change callback
- Consumer<WindowLayoutInfo> consumer = mock(Consumer.class);
+ Consumer<WindowLayoutInfo> consumer = mock(WindowLayoutInfoConsumer.class);
TestActivity activity = mActivityTestRule.launchActivity(new Intent());
backend.registerLayoutChangeCallback(activity, Runnable::run, consumer);
- backend.registerLayoutChangeCallback(activity, Runnable::run, mock(Consumer.class));
+ backend.registerLayoutChangeCallback(activity, Runnable::run,
+ mock(WindowLayoutInfoConsumer.class));
assertEquals(2, backend.mWindowLayoutChangeCallbacks.size());
verify(backend.mWindowExtension).onWindowLayoutChangeListenerAdded(activity);
@@ -174,8 +176,8 @@
backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
// Check registering the layout change callback
- Consumer<WindowLayoutInfo> firstConsumer = mock(Consumer.class);
- Consumer<WindowLayoutInfo> secondConsumer = mock(Consumer.class);
+ Consumer<WindowLayoutInfo> firstConsumer = mock(WindowLayoutInfoConsumer.class);
+ Consumer<WindowLayoutInfo> secondConsumer = mock(WindowLayoutInfoConsumer.class);
TestActivity activity = mActivityTestRule.launchActivity(new Intent());
backend.registerLayoutChangeCallback(activity, Runnable::run, firstConsumer);
backend.registerLayoutChangeCallback(activity, Runnable::run, secondConsumer);
@@ -192,9 +194,10 @@
public void testRegisterLayoutChangeCallback_relayLastEmittedValue() {
WindowLayoutInfo expectedWindowLayoutInfo = newTestWindowLayoutInfo();
ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
- Consumer<WindowLayoutInfo> consumer = mock(Consumer.class);
+ Consumer<WindowLayoutInfo> consumer = mock(WindowLayoutInfoConsumer.class);
TestActivity activity = mActivityTestRule.launchActivity(new Intent());
- backend.registerLayoutChangeCallback(activity, Runnable::run, mock(Consumer.class));
+ backend.registerLayoutChangeCallback(activity, Runnable::run,
+ mock(WindowLayoutInfoConsumer.class));
backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
backend.mLastReportedWindowLayouts.put(activity, expectedWindowLayoutInfo);
@@ -209,7 +212,7 @@
backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
// Check that callbacks from the extension are propagated correctly
- Consumer<WindowLayoutInfo> consumer = mock(Consumer.class);
+ Consumer<WindowLayoutInfo> consumer = mock(WindowLayoutInfoConsumer.class);
TestActivity activity = mActivityTestRule.launchActivity(new Intent());
backend.registerLayoutChangeCallback(activity, Runnable::run, consumer);
@@ -230,13 +233,13 @@
@Test
public void testRegisterDeviceChangeCallback() {
- DeviceState expectedState = newTestDeviceState();
- ExtensionInterfaceCompat mockInterface = mock(ExtensionInterfaceCompat.class);
+ ExtensionInterfaceCompat mockInterface = mock(
+ ExtensionInterfaceCompat.class);
ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
backend.mWindowExtension = mockInterface;
// Check registering the device state change callback
- Consumer<DeviceState> consumer = mock(Consumer.class);
+ Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
assertEquals(1, backend.mDeviceStateChangeCallbacks.size());
@@ -255,7 +258,7 @@
backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
// Check that callbacks from the extension are propagated correctly
- Consumer<DeviceState> consumer = mock(Consumer.class);
+ Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
DeviceState deviceState = newTestDeviceState();
@@ -278,10 +281,10 @@
backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
// Check registering the layout change callback
- Consumer<DeviceState> consumer = mock(Consumer.class);
- TestActivity activity = mActivityTestRule.launchActivity(new Intent());
+ Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
+ mActivityTestRule.launchActivity(new Intent());
backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
- backend.registerDeviceStateChangeCallback(Runnable::run, mock(Consumer.class));
+ backend.registerDeviceStateChangeCallback(Runnable::run, mock(DeviceStateConsumer.class));
assertEquals(2, backend.mDeviceStateChangeCallbacks.size());
verify(backend.mWindowExtension).onDeviceStateListenersChanged(false);
@@ -300,9 +303,9 @@
backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
// Check registering the layout change callback
- Consumer<DeviceState> firstConsumer = mock(Consumer.class);
- Consumer<DeviceState> secondConsumer = mock(Consumer.class);
- TestActivity activity = mActivityTestRule.launchActivity(new Intent());
+ Consumer<DeviceState> firstConsumer = mock(DeviceStateConsumer.class);
+ Consumer<DeviceState> secondConsumer = mock(DeviceStateConsumer.class);
+ mActivityTestRule.launchActivity(new Intent());
backend.registerDeviceStateChangeCallback(Runnable::run, firstConsumer);
backend.registerDeviceStateChangeCallback(Runnable::run, secondConsumer);
@@ -318,7 +321,7 @@
public void testDeviceChangeCallback_relayLastEmittedValue() {
DeviceState expectedState = newTestDeviceState();
ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
- Consumer<DeviceState> consumer = mock(Consumer.class);
+ Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
backend.mLastReportedDeviceState = expectedState;
@@ -330,7 +333,7 @@
@Test
public void testDeviceChangeCallback_clearLastEmittedValue() {
ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
- Consumer<DeviceState> consumer = mock(Consumer.class);
+ Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
backend.unregisterDeviceStateChangeCallback(consumer);
@@ -345,14 +348,10 @@
assertTrue(windowLayoutInfo.getDisplayFeatures().isEmpty());
- DisplayFeature.Builder featureBuilder = new DisplayFeature.Builder();
- featureBuilder.setType(DisplayFeature.TYPE_HINGE);
- featureBuilder.setBounds(new Rect(0, 2, 3, 4));
- DisplayFeature feature1 = featureBuilder.build();
-
- featureBuilder = new DisplayFeature.Builder();
- featureBuilder.setBounds(new Rect(0, 1, 5, 1));
- DisplayFeature feature2 = featureBuilder.build();
+ DisplayFeature feature1 = new FoldingFeature(new Rect(0, 2, 3, 4),
+ FoldingFeature.TYPE_HINGE, FoldingFeature.STATE_FLAT);
+ DisplayFeature feature2 = new FoldingFeature(new Rect(0, 1, 5, 1),
+ FoldingFeature.TYPE_HINGE, FoldingFeature.STATE_FLAT);
List<DisplayFeature> displayFeatures = new ArrayList<>();
displayFeatures.add(feature1);
@@ -369,8 +368,12 @@
return builder.build();
}
+ private interface DeviceStateConsumer extends Consumer<DeviceState> { }
+
+ private interface WindowLayoutInfoConsumer extends Consumer<WindowLayoutInfo> { }
+
private static class SimpleConsumer<T> implements Consumer<T> {
- private List<T> mValues;
+ private final List<T> mValues;
SimpleConsumer() {
mValues = new ArrayList<>();
diff --git a/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java b/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java
new file mode 100644
index 0000000..9339492
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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;
+
+import static androidx.window.FoldingFeature.STATE_FLAT;
+import static androidx.window.FoldingFeature.STATE_FLIPPED;
+import static androidx.window.FoldingFeature.STATE_HALF_OPENED;
+import static androidx.window.FoldingFeature.TYPE_FOLD;
+import static androidx.window.FoldingFeature.TYPE_HINGE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.graphics.Rect;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link FoldingFeature} class. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class FoldingFeatureTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void tesEmptyRect() {
+ new FoldingFeature(new Rect(), TYPE_HINGE, STATE_HALF_OPENED);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testFoldWithNonZeroArea() {
+ new FoldingFeature(new Rect(0, 0, 20, 30), TYPE_FOLD, STATE_FLIPPED);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testHorizontalHingeWithNonZeroOrigin() {
+ new FoldingFeature(new Rect(1, 10, 20, 10), TYPE_HINGE, STATE_FLIPPED);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testVerticalHingeWithNonZeroOrigin() {
+ new FoldingFeature(new Rect(10, 1, 19, 29), TYPE_HINGE, STATE_FLIPPED);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testHorizontalFoldWithNonZeroOrigin() {
+ new FoldingFeature(new Rect(1, 10, 20, 10), TYPE_FOLD, STATE_FLIPPED);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testVerticalFoldWithNonZeroOrigin() {
+ new FoldingFeature(new Rect(10, 1, 10, 20), TYPE_FOLD, STATE_FLIPPED);
+ }
+
+ @Test
+ public void testSetBoundsAndType() {
+ Rect bounds = new Rect(0, 10, 30, 10);
+ int type = TYPE_HINGE;
+ int state = STATE_HALF_OPENED;
+ FoldingFeature feature = new FoldingFeature(bounds, type, state);
+
+ assertEquals(bounds, feature.getBounds());
+ assertEquals(type, feature.getType());
+ assertEquals(state, feature.getState());
+ }
+
+ @Test
+ public void testEquals_sameAttributes() {
+ Rect bounds = new Rect(1, 0, 1, 10);
+ int type = TYPE_FOLD;
+ int state = STATE_FLAT;
+
+ FoldingFeature original = new FoldingFeature(bounds, type, state);
+ FoldingFeature copy = new FoldingFeature(bounds, type, state);
+
+ assertEquals(original, copy);
+ }
+
+ @Test
+ public void testEquals_differentRect() {
+ Rect originalRect = new Rect(1, 0, 1, 10);
+ Rect otherRect = new Rect(2, 0, 2, 10);
+ int type = TYPE_FOLD;
+ int state = STATE_FLAT;
+
+ FoldingFeature original = new FoldingFeature(originalRect, type, state);
+ FoldingFeature other = new FoldingFeature(otherRect, type, state);
+
+ assertNotEquals(original, other);
+ }
+
+ @Test
+ public void testEquals_differentType() {
+ Rect rect = new Rect(1, 0, 1, 10);
+ int originalType = TYPE_FOLD;
+ int otherType = TYPE_HINGE;
+ int state = STATE_FLAT;
+
+ FoldingFeature original = new FoldingFeature(rect, originalType, state);
+ FoldingFeature other = new FoldingFeature(rect, otherType, state);
+
+ assertNotEquals(original, other);
+ }
+
+ @Test
+ public void testEquals_differentState() {
+ Rect rect = new Rect(1, 0, 1, 10);
+ int type = TYPE_FOLD;
+ int originalState = STATE_FLAT;
+ int otherState = STATE_FLIPPED;
+
+ FoldingFeature original = new FoldingFeature(rect, type, originalState);
+ FoldingFeature other = new FoldingFeature(rect, type, otherState);
+
+ assertNotEquals(original, other);
+ }
+
+ @Test
+ public void testHashCode_matchesIfEqual() {
+ Rect originalRect = new Rect(1, 0, 1, 10);
+ Rect matchingRect = new Rect(1, 0, 1, 10);
+ int type = TYPE_FOLD;
+ int state = STATE_FLAT;
+
+ FoldingFeature original = new FoldingFeature(originalRect, type, state);
+ FoldingFeature matching = new FoldingFeature(matchingRect, type, state);
+
+ assertEquals(original, matching);
+ assertEquals(original.hashCode(), matching.hashCode());
+ }
+}
diff --git a/window/window/src/androidTest/java/androidx/window/SidecarAdapterTest.java b/window/window/src/androidTest/java/androidx/window/SidecarAdapterTest.java
index 65e2903..8272c3c 100644
--- a/window/window/src/androidTest/java/androidx/window/SidecarAdapterTest.java
+++ b/window/window/src/androidTest/java/androidx/window/SidecarAdapterTest.java
@@ -16,6 +16,9 @@
package androidx.window;
+import static androidx.window.SidecarAdapter.setSidecarDevicePosture;
+import static androidx.window.SidecarAdapter.setSidecarDisplayFeatures;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
@@ -62,14 +65,13 @@
private static SidecarWindowLayoutInfo sidecarWindowLayoutInfo(
List<SidecarDisplayFeature> features) {
SidecarWindowLayoutInfo layoutInfo = new SidecarWindowLayoutInfo();
- layoutInfo.displayFeatures = new ArrayList<>();
- layoutInfo.displayFeatures.addAll(features);
+ setSidecarDisplayFeatures(layoutInfo, features);
return layoutInfo;
}
private static SidecarDeviceState sidecarDeviceState(int posture) {
SidecarDeviceState deviceState = new SidecarDeviceState();
- deviceState.posture = posture;
+ setSidecarDevicePosture(deviceState, posture);
return deviceState;
}
@@ -85,19 +87,21 @@
sidecarDisplayFeatures.add(foldFeature);
SidecarWindowLayoutInfo windowLayoutInfo = sidecarWindowLayoutInfo(sidecarDisplayFeatures);
+ SidecarDeviceState state = sidecarDeviceState(SidecarDeviceState.POSTURE_OPENED);
+
List<DisplayFeature> expectedFeatures = new ArrayList<>();
- expectedFeatures.add(new DisplayFeature(foldFeature.getRect(), DisplayFeature.TYPE_FOLD));
+ expectedFeatures.add(new FoldingFeature(foldFeature.getRect(), FoldingFeature.TYPE_FOLD,
+ FoldingFeature.STATE_FLAT));
WindowLayoutInfo expected = new WindowLayoutInfo(expectedFeatures);
SidecarAdapter sidecarAdapter = new SidecarAdapter();
- WindowLayoutInfo actual = sidecarAdapter.translate(mockActivity, windowLayoutInfo);
+ WindowLayoutInfo actual = sidecarAdapter.translate(mockActivity, windowLayoutInfo, state);
assertEquals(expected, actual);
}
@Test
- @Override
public void testTranslateWindowLayoutInfo_filterRemovesEmptyBoundsFeature() {
List<SidecarDisplayFeature> sidecarDisplayFeatures = new ArrayList<>();
sidecarDisplayFeatures.add(
@@ -106,15 +110,15 @@
SidecarAdapter sidecarAdapter = new SidecarAdapter();
SidecarWindowLayoutInfo windowLayoutInfo = sidecarWindowLayoutInfo(sidecarDisplayFeatures);
Activity mockActivity = mock(Activity.class);
+ SidecarDeviceState state = sidecarDeviceState(SidecarDeviceState.POSTURE_OPENED);
- WindowLayoutInfo actual = sidecarAdapter.translate(mockActivity, windowLayoutInfo);
+ WindowLayoutInfo actual = sidecarAdapter.translate(mockActivity, windowLayoutInfo, state);
assertTrue(actual.getDisplayFeatures().isEmpty());
}
@Test
- @Override
public void testTranslateWindowLayoutInfo_filterRemovesNonEmptyAreaFoldFeature() {
List<SidecarDisplayFeature> sidecarDisplayFeatures = new ArrayList<>();
Rect fullWidthBounds = new Rect(0, 1, WINDOW_BOUNDS.width(), 2);
@@ -130,7 +134,10 @@
SidecarWindowLayoutInfo windowLayoutInfo = sidecarWindowLayoutInfo(sidecarDisplayFeatures);
Activity mockActivity = mock(Activity.class);
- WindowLayoutInfo actual = sidecarCallbackAdapter.translate(mockActivity, windowLayoutInfo);
+ SidecarDeviceState state = sidecarDeviceState(SidecarDeviceState.POSTURE_OPENED);
+
+ WindowLayoutInfo actual = sidecarCallbackAdapter.translate(mockActivity, windowLayoutInfo,
+ state);
assertTrue(actual.getDisplayFeatures().isEmpty());
}
@@ -151,10 +158,11 @@
SidecarAdapter sidecarAdapter = new SidecarAdapter();
SidecarWindowLayoutInfo windowLayoutInfo = sidecarWindowLayoutInfo(sidecarDisplayFeatures);
+ SidecarDeviceState state = sidecarDeviceState(SidecarDeviceState.POSTURE_OPENED);
Activity mockActivity = mock(Activity.class);
- WindowLayoutInfo actual = sidecarAdapter.translate(mockActivity, windowLayoutInfo);
+ WindowLayoutInfo actual = sidecarAdapter.translate(mockActivity, windowLayoutInfo, state);
assertTrue(actual.getDisplayFeatures().isEmpty());
}
@@ -175,10 +183,12 @@
SidecarAdapter sidecarCallbackAdapter = new SidecarAdapter();
SidecarWindowLayoutInfo windowLayoutInfo = sidecarWindowLayoutInfo(
extensionDisplayFeatures);
+ SidecarDeviceState state = sidecarDeviceState(SidecarDeviceState.POSTURE_OPENED);
Activity mockActivity = mock(Activity.class);
- WindowLayoutInfo actual = sidecarCallbackAdapter.translate(mockActivity, windowLayoutInfo);
+ WindowLayoutInfo actual = sidecarCallbackAdapter.translate(mockActivity, windowLayoutInfo,
+ state);
assertTrue(actual.getDisplayFeatures().isEmpty());
}
@@ -186,8 +196,6 @@
@Test
@Override
public void testTranslateDeviceState() {
- ExtensionInterfaceCompat.ExtensionCallbackInterface mockCallback = mock(
- ExtensionInterfaceCompat.ExtensionCallbackInterface.class);
SidecarAdapter sidecarCallbackAdapter = new SidecarAdapter();
List<DeviceState> values = new ArrayList<>();
diff --git a/window/window/src/androidTest/java/androidx/window/SidecarCompatDeviceTest.java b/window/window/src/androidTest/java/androidx/window/SidecarCompatDeviceTest.java
index 954ed4a..cfc6286 100644
--- a/window/window/src/androidTest/java/androidx/window/SidecarCompatDeviceTest.java
+++ b/window/window/src/androidTest/java/androidx/window/SidecarCompatDeviceTest.java
@@ -17,12 +17,15 @@
package androidx.window;
import static androidx.window.ExtensionInterfaceCompat.ExtensionCallbackInterface;
+import static androidx.window.SidecarAdapter.getSidecarDevicePosture;
+import static androidx.window.SidecarAdapter.getSidecarDisplayFeatures;
import static androidx.window.Version.VERSION_0_1;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -42,6 +45,8 @@
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher;
+import java.util.List;
+
/**
* Tests for {@link SidecarCompat} implementation of {@link ExtensionInterfaceCompat} that are
* executed with Sidecar implementation provided on the device (and only if one is available).
@@ -66,8 +71,8 @@
mSidecarCompat.onDeviceStateListenersChanged(false);
- verify(callbackInterface).onDeviceStateChanged(argThat(
- deviceState -> deviceState.getPosture() == sidecarDeviceState.posture));
+ verify(callbackInterface, atLeastOnce()).onDeviceStateChanged(argThat(deviceState ->
+ deviceState.getPosture() == getSidecarDevicePosture(sidecarDeviceState)));
}
@Test
@@ -83,7 +88,7 @@
SidecarWindowLayoutInfo sidecarWindowLayoutInfo =
mSidecarCompat.mSidecar.getWindowLayoutInfo(windowToken);
- verify(callbackInterface).onWindowLayoutChanged(any(),
+ verify(callbackInterface, atLeastOnce()).onWindowLayoutChanged(any(),
argThat(new SidecarMatcher(sidecarWindowLayoutInfo)));
}
@@ -102,14 +107,16 @@
@Override
public boolean matches(WindowLayoutInfo windowLayoutInfo) {
- if (windowLayoutInfo.getDisplayFeatures().size()
- != mSidecarWindowLayoutInfo.displayFeatures.size()) {
+ List<SidecarDisplayFeature> sidecarDisplayFeatures =
+ getSidecarDisplayFeatures(mSidecarWindowLayoutInfo);
+ if (windowLayoutInfo.getDisplayFeatures().size() != sidecarDisplayFeatures.size()) {
return false;
}
for (int i = 0; i < windowLayoutInfo.getDisplayFeatures().size(); i++) {
- DisplayFeature feature = windowLayoutInfo.getDisplayFeatures().get(i);
- SidecarDisplayFeature sidecarDisplayFeature =
- mSidecarWindowLayoutInfo.displayFeatures.get(i);
+ // Sidecar only has folding features
+ FoldingFeature feature = (FoldingFeature) windowLayoutInfo.getDisplayFeatures()
+ .get(i);
+ SidecarDisplayFeature sidecarDisplayFeature = sidecarDisplayFeatures.get(i);
if (feature.getType() != sidecarDisplayFeature.getType()) {
return false;
diff --git a/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java b/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java
index 05f6c7d..5acba49 100644
--- a/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java
+++ b/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java
@@ -16,16 +16,23 @@
package androidx.window;
-import static androidx.window.TestBoundUtil.invalidFoldBounds;
-import static androidx.window.TestBoundUtil.invalidHingeBounds;
-import static androidx.window.TestBoundUtil.validFoldBound;
+import static androidx.window.SidecarAdapter.getSidecarDisplayFeatures;
+import static androidx.window.SidecarAdapter.setSidecarDevicePosture;
+import static androidx.window.SidecarAdapter.setSidecarDisplayFeatures;
+import static androidx.window.TestBoundsUtil.invalidFoldBounds;
+import static androidx.window.TestBoundsUtil.invalidHingeBounds;
+import static androidx.window.TestBoundsUtil.validFoldBound;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -54,6 +61,7 @@
import org.mockito.ArgumentCaptor;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
/**
@@ -89,14 +97,15 @@
// Setup mocked sidecar responses
SidecarDeviceState defaultDeviceState = new SidecarDeviceState();
- defaultDeviceState.posture = SidecarDeviceState.POSTURE_HALF_OPENED;
+ setSidecarDevicePosture(defaultDeviceState, SidecarDeviceState.POSTURE_HALF_OPENED);
when(mSidecarCompat.mSidecar.getDeviceState()).thenReturn(defaultDeviceState);
SidecarDisplayFeature sidecarDisplayFeature = newDisplayFeature(
new Rect(0, 1, WINDOW_BOUNDS.width(), 1), SidecarDisplayFeature.TYPE_HINGE);
SidecarWindowLayoutInfo sidecarWindowLayoutInfo = new SidecarWindowLayoutInfo();
- sidecarWindowLayoutInfo.displayFeatures = new ArrayList<>();
- sidecarWindowLayoutInfo.displayFeatures.add(sidecarDisplayFeature);
+ List<SidecarDisplayFeature> displayFeatures = new ArrayList<>();
+ displayFeatures.add(sidecarDisplayFeature);
+ setSidecarDisplayFeatures(sidecarWindowLayoutInfo, displayFeatures);
when(mSidecarCompat.mSidecar.getWindowLayoutInfo(any()))
.thenReturn(sidecarWindowLayoutInfo);
}
@@ -109,23 +118,27 @@
@Test
@Override
public void testGetDeviceState() {
- FakeExtensionImp fakeSidecarImp = new FakeExtensionImp();
+ FakeExtensionImp fakeSidecarImp = new FakeExtensionImp(
+ newDeviceState(SidecarDeviceState.POSTURE_OPENED),
+ newWindowLayoutInfo(Collections.emptyList()));
SidecarCompat compat = new SidecarCompat(fakeSidecarImp, new SidecarAdapter());
ExtensionInterfaceCompat.ExtensionCallbackInterface mockCallback = mock(
ExtensionInterfaceCompat.ExtensionCallbackInterface.class);
compat.setExtensionCallback(mockCallback);
compat.onDeviceStateListenersChanged(false);
- SidecarDeviceState deviceState = newDeviceState(SidecarDeviceState.POSTURE_OPENED);
+ SidecarDeviceState deviceState = newDeviceState(SidecarDeviceState.POSTURE_HALF_OPENED);
fakeSidecarImp.triggerDeviceState(deviceState);
- verify(mockCallback).onDeviceStateChanged(any());
+ verify(mockCallback, atLeastOnce()).onDeviceStateChanged(any());
}
@Test
@Override
public void testGetWindowLayout() {
- FakeExtensionImp fakeSidecarImp = new FakeExtensionImp();
+ FakeExtensionImp fakeSidecarImp = new FakeExtensionImp(
+ newDeviceState(SidecarDeviceState.POSTURE_OPENED),
+ newWindowLayoutInfo(Collections.emptyList()));
SidecarCompat compat = new SidecarCompat(fakeSidecarImp, new SidecarAdapter());
ExtensionInterfaceCompat.ExtensionCallbackInterface mockCallback = mock(
ExtensionInterfaceCompat.ExtensionCallbackInterface.class);
@@ -134,7 +147,8 @@
fakeSidecarImp.triggerGoodSignal();
- verify(mockCallback).onWindowLayoutChanged(any(), any());
+ verify(mockCallback, atLeastOnce()).onWindowLayoutChanged(eq(mActivity),
+ any(WindowLayoutInfo.class));
}
@Test
@@ -143,7 +157,7 @@
SidecarWindowLayoutInfo originalWindowLayoutInfo =
mSidecarCompat.mSidecar.getWindowLayoutInfo(getActivityWindowToken(mActivity));
List<SidecarDisplayFeature> sidecarDisplayFeatures =
- originalWindowLayoutInfo.displayFeatures;
+ getSidecarDisplayFeatures(originalWindowLayoutInfo);
SidecarDisplayFeature newFeature = new SidecarDisplayFeature();
newFeature.setRect(new Rect());
sidecarDisplayFeatures.add(newFeature);
@@ -160,7 +174,7 @@
SidecarWindowLayoutInfo originalWindowLayoutInfo =
mSidecarCompat.mSidecar.getWindowLayoutInfo(mock(IBinder.class));
List<SidecarDisplayFeature> sidecarDisplayFeatures =
- originalWindowLayoutInfo.displayFeatures;
+ getSidecarDisplayFeatures(originalWindowLayoutInfo);
// Horizontal fold.
sidecarDisplayFeatures.add(
newDisplayFeature(new Rect(0, 1, WINDOW_BOUNDS.width(), 2),
@@ -183,7 +197,7 @@
SidecarWindowLayoutInfo originalWindowLayoutInfo =
mSidecarCompat.mSidecar.getWindowLayoutInfo(mock(IBinder.class));
List<SidecarDisplayFeature> sidecarDisplayFeatures =
- originalWindowLayoutInfo.displayFeatures;
+ getSidecarDisplayFeatures(originalWindowLayoutInfo);
// Horizontal hinge.
sidecarDisplayFeatures.add(
newDisplayFeature(new Rect(0, 1, WINDOW_BOUNDS.width() - 1, 2),
@@ -206,7 +220,7 @@
SidecarWindowLayoutInfo originalWindowLayoutInfo =
mSidecarCompat.mSidecar.getWindowLayoutInfo(mock(IBinder.class));
List<SidecarDisplayFeature> sidecarDisplayFeatures =
- originalWindowLayoutInfo.displayFeatures;
+ getSidecarDisplayFeatures(originalWindowLayoutInfo);
// Horizontal fold.
sidecarDisplayFeatures.add(
newDisplayFeature(new Rect(0, 1, WINDOW_BOUNDS.width() - 1, 2),
@@ -226,7 +240,9 @@
@Override
public void testExtensionCallback_filterRemovesInvalidValues() {
- FakeExtensionImp fakeSidecarImp = new FakeExtensionImp();
+ FakeExtensionImp fakeSidecarImp = new FakeExtensionImp(
+ newDeviceState(SidecarDeviceState.POSTURE_OPENED),
+ newWindowLayoutInfo(Collections.emptyList()));
SidecarCompat compat = new SidecarCompat(fakeSidecarImp, new SidecarAdapter());
ExtensionInterfaceCompat.ExtensionCallbackInterface mockCallback = mock(
ExtensionInterfaceCompat.ExtensionCallbackInterface.class);
@@ -254,7 +270,7 @@
// Verify that the callback set for sidecar propagates the device state callback
SidecarDeviceState sidecarDeviceState = new SidecarDeviceState();
- sidecarDeviceState.posture = SidecarDeviceState.POSTURE_HALF_OPENED;
+ setSidecarDevicePosture(sidecarDeviceState, SidecarDeviceState.POSTURE_HALF_OPENED);
sidecarCallbackCaptor.getValue().onDeviceStateChanged(sidecarDeviceState);
ArgumentCaptor<DeviceState> deviceStateCaptor = ArgumentCaptor.forClass(DeviceState.class);
@@ -268,8 +284,9 @@
SidecarDisplayFeature sidecarDisplayFeature = newDisplayFeature(bounds,
SidecarDisplayFeature.TYPE_HINGE);
SidecarWindowLayoutInfo sidecarWindowLayoutInfo = new SidecarWindowLayoutInfo();
- sidecarWindowLayoutInfo.displayFeatures = new ArrayList<>();
- sidecarWindowLayoutInfo.displayFeatures.add(sidecarDisplayFeature);
+ List<SidecarDisplayFeature> displayFeatures = new ArrayList<>();
+ displayFeatures.add(sidecarDisplayFeature);
+ setSidecarDisplayFeatures(sidecarWindowLayoutInfo, displayFeatures);
sidecarCallbackCaptor.getValue().onWindowLayoutChanged(getActivityWindowToken(mActivity),
sidecarWindowLayoutInfo);
@@ -281,7 +298,9 @@
WindowLayoutInfo capturedLayout = windowLayoutInfoCaptor.getValue();
assertEquals(1, capturedLayout.getDisplayFeatures().size());
DisplayFeature capturedDisplayFeature = capturedLayout.getDisplayFeatures().get(0);
- assertEquals(DisplayFeature.TYPE_HINGE, capturedDisplayFeature.getType());
+ FoldingFeature foldingFeature = (FoldingFeature) capturedDisplayFeature;
+ assertNotNull(foldingFeature);
+ assertEquals(FoldingFeature.TYPE_HINGE, foldingFeature.getType());
assertEquals(bounds, capturedDisplayFeature.getBounds());
}
@@ -304,8 +323,9 @@
Rect bounds = new Rect(1, 2, 3, 4);
sidecarDisplayFeature.setRect(bounds);
SidecarWindowLayoutInfo sidecarWindowLayoutInfo = new SidecarWindowLayoutInfo();
- sidecarWindowLayoutInfo.displayFeatures = new ArrayList<>();
- sidecarWindowLayoutInfo.displayFeatures.add(sidecarDisplayFeature);
+ List<SidecarDisplayFeature> displayFeatures = new ArrayList<>();
+ displayFeatures.add(sidecarDisplayFeature);
+ setSidecarDisplayFeatures(sidecarWindowLayoutInfo, displayFeatures);
IBinder windowToken = mock(IBinder.class);
sidecarCallbackCaptor.getValue().onWindowLayoutChanged(windowToken,
@@ -345,9 +365,8 @@
when(mSidecarCompat.mSidecar.getWindowLayoutInfo(any())).thenReturn(layoutInfo);
View fakeView = mock(View.class);
doAnswer(invocation -> {
- View.OnAttachStateChangeListener stateChangeListener =
- (View.OnAttachStateChangeListener) invocation.getArgument(0);
- stateChangeListener.onViewAttachedToWindow((View) invocation.getMock());
+ View.OnAttachStateChangeListener stateChangeListener = invocation.getArgument(0);
+ stateChangeListener.onViewAttachedToWindow(fakeView);
return null;
}).when(fakeView).addOnAttachStateChangeListener(any());
Window fakeWindow = new TestWindow(mActivity, fakeView);
@@ -387,6 +406,47 @@
verify(listener).onDeviceStateChanged(expectedDeviceState);
}
+ @Test
+ public void testOnDeviceStateChangedUpdatesWindowLayout() {
+ FakeExtensionImp fakeSidecarImp = new FakeExtensionImp(
+ newDeviceState(SidecarDeviceState.POSTURE_CLOSED),
+ validWindowLayoutInfo());
+ SidecarCompat compat = new SidecarCompat(fakeSidecarImp, new SidecarAdapter());
+ ExtensionInterfaceCompat.ExtensionCallbackInterface mockCallback = mock(
+ ExtensionInterfaceCompat.ExtensionCallbackInterface.class);
+ compat.setExtensionCallback(mockCallback);
+ compat.onWindowLayoutChangeListenerAdded(mActivity);
+ ArgumentCaptor<WindowLayoutInfo> windowLayoutCaptor = ArgumentCaptor.forClass(
+ WindowLayoutInfo.class);
+
+ reset(mockCallback);
+ fakeSidecarImp.triggerDeviceState(newDeviceState(SidecarDeviceState.POSTURE_OPENED));
+ verify(mockCallback).onWindowLayoutChanged(eq(mActivity), windowLayoutCaptor.capture());
+ FoldingFeature capturedFoldingFeature = (FoldingFeature) windowLayoutCaptor.getValue()
+ .getDisplayFeatures().get(0);
+ assertEquals(FoldingFeature.STATE_FLAT, capturedFoldingFeature.getState());
+
+ reset(mockCallback);
+ fakeSidecarImp.triggerDeviceState(newDeviceState(SidecarDeviceState.POSTURE_HALF_OPENED));
+ verify(mockCallback).onWindowLayoutChanged(eq(mActivity), windowLayoutCaptor.capture());
+ capturedFoldingFeature = (FoldingFeature) windowLayoutCaptor.getValue().getDisplayFeatures()
+ .get(0);
+ assertEquals(FoldingFeature.STATE_HALF_OPENED, capturedFoldingFeature.getState());
+
+ reset(mockCallback);
+ fakeSidecarImp.triggerDeviceState(newDeviceState(SidecarDeviceState.POSTURE_FLIPPED));
+ verify(mockCallback).onWindowLayoutChanged(eq(mActivity), windowLayoutCaptor.capture());
+ capturedFoldingFeature = (FoldingFeature) windowLayoutCaptor.getValue().getDisplayFeatures()
+ .get(0);
+ assertEquals(FoldingFeature.STATE_FLIPPED, capturedFoldingFeature.getState());
+
+ // No display features must be reported in closed state
+ reset(mockCallback);
+ fakeSidecarImp.triggerDeviceState(newDeviceState(SidecarDeviceState.POSTURE_CLOSED));
+ verify(mockCallback).onWindowLayoutChanged(eq(mActivity), windowLayoutCaptor.capture());
+ assertTrue(windowLayoutCaptor.getValue().getDisplayFeatures().isEmpty());
+ }
+
private static SidecarDisplayFeature newDisplayFeature(Rect rect, int type) {
SidecarDisplayFeature feature = new SidecarDisplayFeature();
feature.setRect(rect);
@@ -397,23 +457,36 @@
private static SidecarWindowLayoutInfo newWindowLayoutInfo(
List<SidecarDisplayFeature> features) {
SidecarWindowLayoutInfo info = new SidecarWindowLayoutInfo();
- info.displayFeatures = new ArrayList<>();
- info.displayFeatures.addAll(features);
+ setSidecarDisplayFeatures(info, features);
return info;
}
+ private static SidecarWindowLayoutInfo validWindowLayoutInfo() {
+ List<SidecarDisplayFeature> goodFeatures = new ArrayList<>();
+
+ goodFeatures.add(newDisplayFeature(validFoldBound(WINDOW_BOUNDS),
+ SidecarDisplayFeature.TYPE_FOLD));
+
+ return newWindowLayoutInfo(goodFeatures);
+ }
+
private static SidecarDeviceState newDeviceState(int posture) {
SidecarDeviceState state = new SidecarDeviceState();
- state.posture = posture;
+ setSidecarDevicePosture(state, posture);
return state;
}
private static final class FakeExtensionImp implements SidecarInterface {
+ private SidecarDeviceState mDeviceState;
+ private SidecarWindowLayoutInfo mInfo;
private SidecarInterface.SidecarCallback mCallback;
private List<IBinder> mTokens = new ArrayList<>();
- FakeExtensionImp() {
+ FakeExtensionImp(@NonNull SidecarDeviceState deviceState,
+ @NonNull SidecarWindowLayoutInfo info) {
+ mDeviceState = deviceState;
+ mInfo = info;
mCallback = new SidecarInterface.SidecarCallback() {
@Override
public void onDeviceStateChanged(@NonNull SidecarDeviceState newDeviceState) {
@@ -430,29 +503,29 @@
@Override
public void setSidecarCallback(@NonNull SidecarCallback callback) {
-
+ mCallback = callback;
}
@NonNull
@Override
public SidecarWindowLayoutInfo getWindowLayoutInfo(@NonNull IBinder windowToken) {
- return null;
+ return mInfo;
}
@Override
public void onWindowLayoutChangeListenerAdded(@NonNull IBinder windowToken) {
-
+ mTokens.add(windowToken);
}
@Override
public void onWindowLayoutChangeListenerRemoved(@NonNull IBinder windowToken) {
-
+ mTokens.remove(windowToken);
}
@NonNull
@Override
public SidecarDeviceState getDeviceState() {
- return null;
+ return mDeviceState;
}
@Override
@@ -469,14 +542,15 @@
}
void triggerSignal(SidecarWindowLayoutInfo info) {
- for (IBinder token: mTokens) {
+ mInfo = info;
+ for (IBinder token : mTokens) {
mCallback.onWindowLayoutChanged(token, info);
}
}
public void triggerDeviceState(SidecarDeviceState state) {
+ mDeviceState = state;
mCallback.onDeviceStateChanged(state);
-
}
private SidecarWindowLayoutInfo malformedWindowLayoutInfo() {
@@ -494,14 +568,5 @@
return newWindowLayoutInfo(malformedFeatures);
}
-
- private SidecarWindowLayoutInfo validWindowLayoutInfo() {
- List<SidecarDisplayFeature> goodFeatures = new ArrayList<>();
-
- goodFeatures.add(newDisplayFeature(validFoldBound(WINDOW_BOUNDS),
- SidecarDisplayFeature.TYPE_FOLD));
-
- return newWindowLayoutInfo(goodFeatures);
- }
}
}
diff --git a/window/window/src/androidTest/java/androidx/window/TestActivity.java b/window/window/src/androidTest/java/androidx/window/TestActivity.java
index ee73aac..65d1a81 100644
--- a/window/window/src/androidTest/java/androidx/window/TestActivity.java
+++ b/window/window/src/androidTest/java/androidx/window/TestActivity.java
@@ -28,7 +28,7 @@
public class TestActivity extends Activity implements View.OnLayoutChangeListener {
private int mRootViewId;
- private CountDownLatch mLayoutLatch;
+ private CountDownLatch mLayoutLatch = new CountDownLatch(1);
private static CountDownLatch sResumeLatch = new CountDownLatch(1);
@Override
@@ -39,18 +39,9 @@
contentView.setId(mRootViewId);
setContentView(contentView);
- resetLayoutCounter();
getWindow().getDecorView().addOnLayoutChangeListener(this);
}
- int getWidth() {
- return findViewById(mRootViewId).getWidth();
- }
-
- int getHeight() {
- return findViewById(mRootViewId).getHeight();
- }
-
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
int oldTop, int oldRight, int oldBottom) {
diff --git a/window/window/src/androidTest/java/androidx/window/TestBoundUtil.java b/window/window/src/androidTest/java/androidx/window/TestBoundsUtil.java
similarity index 79%
copy from window/window/src/androidTest/java/androidx/window/TestBoundUtil.java
copy to window/window/src/androidTest/java/androidx/window/TestBoundsUtil.java
index 18447f5..ff353db 100644
--- a/window/window/src/androidTest/java/androidx/window/TestBoundUtil.java
+++ b/window/window/src/androidTest/java/androidx/window/TestBoundsUtil.java
@@ -24,10 +24,11 @@
/**
* A utility class to provide bounds for a display feature
*/
-class TestBoundUtil {
+class TestBoundsUtil {
public static Rect validFoldBound(Rect windowBounds) {
- return new Rect(windowBounds.left, windowBounds.top, windowBounds.right, 0);
+ int verticalMid = windowBounds.top + windowBounds.height() / 2;
+ return new Rect(0, verticalMid, windowBounds.width(), verticalMid);
}
public static Rect invalidZeroBound() {
@@ -35,11 +36,11 @@
}
public static Rect invalidBoundShortWidth(Rect windowBounds) {
- return new Rect(windowBounds.left, windowBounds.top, windowBounds.right / 2, 2);
+ return new Rect(0, 0, windowBounds.width() / 2, 2);
}
- public static Rect invalidBoundShortHeightHeight(Rect windowBounds) {
- return new Rect(windowBounds.left, windowBounds.top, 2, windowBounds.bottom / 2);
+ public static Rect invalidBoundShortHeight(Rect windowBounds) {
+ return new Rect(0, 0, 2, windowBounds.height() / 2);
}
private static List<Rect> coreInvalidBounds(Rect windowBounds) {
@@ -47,7 +48,7 @@
badBounds.add(invalidZeroBound());
badBounds.add(invalidBoundShortWidth(windowBounds));
- badBounds.add(invalidBoundShortHeightHeight(windowBounds));
+ badBounds.add(invalidBoundShortHeight(windowBounds));
return badBounds;
}
diff --git a/window/window/src/androidTest/java/androidx/window/TranslatorTestInterface.java b/window/window/src/androidTest/java/androidx/window/TranslatorTestInterface.java
index 90eb92b..958063b 100644
--- a/window/window/src/androidTest/java/androidx/window/TranslatorTestInterface.java
+++ b/window/window/src/androidTest/java/androidx/window/TranslatorTestInterface.java
@@ -23,8 +23,6 @@
public interface TranslatorTestInterface {
void testTranslate_validFeature();
void testTranslateDeviceState();
- void testTranslateWindowLayoutInfo_filterRemovesEmptyBoundsFeature();
- void testTranslateWindowLayoutInfo_filterRemovesNonEmptyAreaFoldFeature();
void testTranslateWindowLayoutInfo_filterRemovesHingeFeatureNotSpanningFullDimension();
void testTranslateWindowLayoutInfo_filterRemovesFoldFeatureNotSpanningFullDimension();
}
diff --git a/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java b/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java
index 6378d7a..d807de5 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java
+++ b/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java
@@ -65,8 +65,9 @@
private WindowLayoutInfo newTestWindowLayout() {
List<DisplayFeature> displayFeatureList = new ArrayList<>();
- DisplayFeature displayFeature = new DisplayFeature(
- new Rect(10, 0, 10, 100), DisplayFeature.TYPE_HINGE);
+ DisplayFeature displayFeature = new FoldingFeature(
+ new Rect(10, 0, 10, 100), FoldingFeature.TYPE_HINGE,
+ FoldingFeature.STATE_FLAT);
displayFeatureList.add(displayFeature);
return new WindowLayoutInfo(displayFeatureList);
}
diff --git a/window/window/src/androidTest/java/androidx/window/WindowBoundsHelperTest.java b/window/window/src/androidTest/java/androidx/window/WindowBoundsHelperTest.java
index cf6eab3..5db5039 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowBoundsHelperTest.java
+++ b/window/window/src/androidTest/java/androidx/window/WindowBoundsHelperTest.java
@@ -33,6 +33,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
+import org.junit.AssumptionViolatedException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -48,10 +49,9 @@
@Test
public void testGetCurrentWindowBounds_matchParentWindowSize_avoidCutouts_preR() {
assumePlatformBeforeR();
+ assumeNotMultiWindow();
testGetCurrentWindowBoundsMatchesRealDisplaySize(activity -> {
- assumeFalse(isInMultiWindowMode(activity));
-
WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
lp.width = WindowManager.LayoutParams.MATCH_PARENT;
lp.height = WindowManager.LayoutParams.MATCH_PARENT;
@@ -66,10 +66,9 @@
@Test
public void testGetCurrentWindowBounds_fixedWindowSize_avoidCutouts_preR() {
assumePlatformBeforeR();
+ assumeNotMultiWindow();
testGetCurrentWindowBoundsMatchesRealDisplaySize(activity -> {
- assumeFalse(isInMultiWindowMode(activity));
-
WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
lp.width = 100;
lp.height = 100;
@@ -84,10 +83,9 @@
@Test
public void testGetCurrentWindowBounds_matchParentWindowSize_layoutBehindCutouts_preR() {
assumePlatformBeforeR();
+ assumeNotMultiWindow();
testGetCurrentWindowBoundsMatchesRealDisplaySize(activity -> {
- assumeFalse(isInMultiWindowMode(activity));
-
WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
lp.width = WindowManager.LayoutParams.MATCH_PARENT;
lp.height = WindowManager.LayoutParams.MATCH_PARENT;
@@ -102,10 +100,9 @@
@Test
public void testGetCurrentWindowBounds_fixedWindowSize_layoutBehindCutouts_preR() {
assumePlatformBeforeR();
+ assumeNotMultiWindow();
testGetCurrentWindowBoundsMatchesRealDisplaySize(activity -> {
- assumeFalse(isInMultiWindowMode(activity));
-
WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
lp.width = 100;
lp.height = 100;
@@ -132,10 +129,9 @@
@Test
public void testGetMaximumWindowBounds_matchParentWindowSize_avoidCutouts_preR() {
assumePlatformBeforeR();
+ assumeNotMultiWindow();
testGetMaximumWindowBoundsMatchesRealDisplaySize(activity -> {
- assumeFalse(isInMultiWindowMode(activity));
-
WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
lp.width = WindowManager.LayoutParams.MATCH_PARENT;
lp.height = WindowManager.LayoutParams.MATCH_PARENT;
@@ -150,10 +146,9 @@
@Test
public void testGetMaximumWindowBounds_fixedWindowSize_avoidCutouts_preR() {
assumePlatformBeforeR();
+ assumeNotMultiWindow();
testGetMaximumWindowBoundsMatchesRealDisplaySize(activity -> {
- assumeFalse(isInMultiWindowMode(activity));
-
WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
lp.width = 100;
lp.height = 100;
@@ -168,10 +163,9 @@
@Test
public void testGetMaximumWindowBounds_matchParentWindowSize_layoutBehindCutouts_preR() {
assumePlatformBeforeR();
+ assumeNotMultiWindow();
testGetMaximumWindowBoundsMatchesRealDisplaySize(activity -> {
- assumeFalse(isInMultiWindowMode(activity));
-
WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
lp.width = WindowManager.LayoutParams.MATCH_PARENT;
lp.height = WindowManager.LayoutParams.MATCH_PARENT;
@@ -186,10 +180,9 @@
@Test
public void testGetMaximumWindowBounds_fixedWindowSize_layoutBehindCutouts_preR() {
assumePlatformBeforeR();
+ assumeNotMultiWindow();
testGetMaximumWindowBoundsMatchesRealDisplaySize(activity -> {
- assumeFalse(isInMultiWindowMode(activity));
-
WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
lp.width = 100;
lp.height = 100;
@@ -250,6 +243,20 @@
scenario.onActivity(verifyAction);
}
+ private void assumeNotMultiWindow() {
+ ActivityScenario<TestActivity> scenario = mActivityScenarioRule.getScenario();
+ try {
+ scenario.onActivity(activity -> assumeFalse(isInMultiWindowMode(activity)));
+ } catch (RuntimeException e) {
+ if (e.getCause() instanceof AssumptionViolatedException) {
+ AssumptionViolatedException failedAssumption =
+ (AssumptionViolatedException) e.getCause();
+ throw failedAssumption;
+ }
+ throw e;
+ }
+ }
+
private static boolean isInMultiWindowMode(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return activity.isInMultiWindowMode();
diff --git a/window/window/src/androidTest/java/androidx/window/WindowLayoutInfoTest.java b/window/window/src/androidTest/java/androidx/window/WindowLayoutInfoTest.java
index 8c97849..62154b1 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowLayoutInfoTest.java
+++ b/window/window/src/androidTest/java/androidx/window/WindowLayoutInfoTest.java
@@ -48,14 +48,11 @@
@Test
public void testBuilder_setDisplayFeatures() {
- DisplayFeature.Builder featureBuilder = new DisplayFeature.Builder();
- featureBuilder.setType(DisplayFeature.TYPE_HINGE);
- featureBuilder.setBounds(new Rect(1, 0, 3, 4));
- DisplayFeature feature1 = featureBuilder.build();
+ DisplayFeature feature1 = new FoldingFeature(new Rect(1, 0, 3, 4),
+ FoldingFeature.TYPE_HINGE, FoldingFeature.STATE_FLAT);
- featureBuilder = new DisplayFeature.Builder();
- featureBuilder.setBounds(new Rect(1, 0, 1, 4));
- DisplayFeature feature2 = featureBuilder.build();
+ DisplayFeature feature2 = new FoldingFeature(new Rect(1, 0, 1, 4),
+ FoldingFeature.STATE_FLAT, FoldingFeature.STATE_FLAT);
List<DisplayFeature> displayFeatures = new ArrayList<>();
displayFeatures.add(feature1);
@@ -83,7 +80,8 @@
List<DisplayFeature> originalFeatures = new ArrayList<>();
List<DisplayFeature> differentFeatures = new ArrayList<>();
Rect rect = new Rect(1, 0, 1, 10);
- differentFeatures.add(new DisplayFeature(rect, 1));
+ differentFeatures.add(new FoldingFeature(rect, FoldingFeature.TYPE_HINGE,
+ FoldingFeature.STATE_FLAT));
WindowLayoutInfo original = new WindowLayoutInfo(originalFeatures);
WindowLayoutInfo different = new WindowLayoutInfo(differentFeatures);
@@ -104,13 +102,15 @@
@Test
public void testHashCode_matchesIfEqualFeatures() {
- DisplayFeature originalFeature = new DisplayFeature(
- new Rect(-1, 1, 1, -1),
- 0
+ DisplayFeature originalFeature = new FoldingFeature(
+ new Rect(0, 0, 100, 0),
+ FoldingFeature.TYPE_HINGE,
+ FoldingFeature.STATE_FLAT
);
- DisplayFeature matchingFeature = new DisplayFeature(
- new Rect(-1, 1, 1, -1),
- 0
+ DisplayFeature matchingFeature = new FoldingFeature(
+ new Rect(0, 0, 100, 0),
+ FoldingFeature.TYPE_HINGE,
+ FoldingFeature.STATE_FLAT
);
List<DisplayFeature> firstFeatures = Collections.singletonList(originalFeature);
List<DisplayFeature> secondFeatures = Collections.singletonList(matchingFeature);
diff --git a/window/window/src/main/java/androidx/window/DeviceState.java b/window/window/src/main/java/androidx/window/DeviceState.java
index d031629..5584f71 100644
--- a/window/window/src/main/java/androidx/window/DeviceState.java
+++ b/window/window/src/main/java/androidx/window/DeviceState.java
@@ -23,9 +23,12 @@
import java.lang.annotation.RetentionPolicy;
/**
+ * @deprecated Use {@link FoldingFeature} to get the state of the hinge instead. Will be removed in
+ * alpha03.
* Information about the state of the device.
* <p>Currently only includes the description of the state for foldable devices.
*/
+@Deprecated
public final class DeviceState {
@Posture
diff --git a/window/window/src/main/java/androidx/window/DisplayFeature.java b/window/window/src/main/java/androidx/window/DisplayFeature.java
index efa0ceb..09d9491 100644
--- a/window/window/src/main/java/androidx/window/DisplayFeature.java
+++ b/window/window/src/main/java/androidx/window/DisplayFeature.java
@@ -18,12 +18,8 @@
import android.graphics.Rect;
-import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
/**
* Description of a physical feature on the display.
*
@@ -31,162 +27,15 @@
* the device. It can intrude into the application window space and create a visual distortion,
* visual or touch discontinuity, make some area invisible or create a logical divider or separation
* in the screen space.
- *
- * @see #TYPE_FOLD
- * @see #TYPE_HINGE
*/
-public final class DisplayFeature {
- private final Rect mBounds;
- @Type
- private int mType;
-
- DisplayFeature(@NonNull Rect bounds, @Type int type) {
- assertValidBounds(bounds, type);
- mBounds = new Rect(bounds);
- mType = type;
- }
+public interface DisplayFeature {
/**
- * Gets bounding rectangle of the physical display feature in the coordinate space of the
- * window. The rectangle provides information about the location of the feature in the window
- * and its size.
+ * The bounding rectangle of the feature within the application window
+ * in the window coordinate space.
*
- * <p>The bounds for features of type {@link #TYPE_FOLD fold} are guaranteed to be zero-high
- * (for horizontal folds) or zero-wide (for vertical folds) and span the entire window.
- *
- * <p>The bounds for features of type {@link #TYPE_HINGE hinge} are guaranteed to span the
- * entire window but, unlike folds, can have a non-zero area.
+ * @return bounds of display feature.
*/
@NonNull
- public Rect getBounds() {
- return new Rect(mBounds);
- }
-
- /**
- * Gets type of the physical display feature.
- */
- @Type
- public int getType() {
- return mType;
- }
-
- /**
- * A fold in the flexible screen without a physical gap.
- */
- public static final int TYPE_FOLD = 1;
-
- /**
- * A physical separation with a hinge that allows two display panels to fold.
- */
- public static final int TYPE_HINGE = 2;
-
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({
- TYPE_FOLD,
- TYPE_HINGE,
- })
- @interface Type{}
-
- private static String typeToString(@Type int type) {
- switch (type) {
- case TYPE_FOLD:
- return "FOLD";
- case TYPE_HINGE:
- return "HINGE";
- default:
- return "Unknown feature type (" + type + ")";
- }
- }
-
- @NonNull
- @Override
- public String toString() {
- return "DisplayFeature{ bounds=" + mBounds + ", type=" + typeToString(mType) + " }";
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- DisplayFeature that = (DisplayFeature) o;
-
- return mType == that.mType && mBounds.equals(that.mBounds);
- }
-
- @Override
- public int hashCode() {
- int result = mBounds.hashCode();
- result = 31 * result + mType;
- return result;
- }
-
- /**
- * Builder for {@link DisplayFeature} objects.
- */
- public static final class Builder {
- private Rect mBounds = new Rect();
- @Type
- private int mType = TYPE_FOLD;
-
- /**
- * Creates an initially empty builder.
- */
- public Builder() {
- }
-
- /**
- * Sets the bounds for the {@link DisplayFeature} instance.
- */
- @NonNull
- public Builder setBounds(@NonNull Rect bounds) {
- mBounds = bounds;
- return this;
- }
-
- /**
- * Sets the type for the {@link DisplayFeature} instance.
- */
- @NonNull
- public Builder setType(@Type int type) {
- mType = type;
- return this;
- }
-
- /**
- * Creates a {@link DisplayFeature} instance with the specified fields.
- * @return A DisplayFeature instance.
- */
- @NonNull
- public DisplayFeature build() {
- return new DisplayFeature(mBounds, mType);
- }
- }
-
- /**
- * Throws runtime exceptions if the bounds are invalid or incompatible with the supplied type.
- */
- private static void assertValidBounds(Rect bounds, @Type int type) {
- if (bounds.height() == 0 && bounds.width() == 0) {
- throw new IllegalArgumentException("Bounding rectangle must not be empty: " + bounds);
- }
-
- if (type == TYPE_FOLD) {
- if (bounds.width() != 0 && bounds.height() != 0) {
- throw new IllegalArgumentException("Bounding rectangle must be either zero-wide "
- + "or zero-high for features of type " + typeToString(type));
- }
-
- if ((bounds.width() != 0 && bounds.left != 0)
- || (bounds.height() != 0 && bounds.top != 0)) {
- throw new IllegalArgumentException("Bounding rectangle must span the entire "
- + "window space for features of type " + typeToString(type));
- }
- } else if (type == TYPE_HINGE) {
- if (bounds.left != 0 && bounds.top != 0) {
- throw new IllegalArgumentException("Bounding rectangle must span the entire "
- + "window space for features of type " + typeToString(type));
- }
- }
- }
+ Rect getBounds();
}
diff --git a/window/window/src/main/java/androidx/window/ExtensionAdapter.java b/window/window/src/main/java/androidx/window/ExtensionAdapter.java
index c2e5549..d3150e2 100644
--- a/window/window/src/main/java/androidx/window/ExtensionAdapter.java
+++ b/window/window/src/main/java/androidx/window/ExtensionAdapter.java
@@ -23,6 +23,7 @@
import androidx.annotation.Nullable;
import androidx.window.extensions.ExtensionDeviceState;
import androidx.window.extensions.ExtensionDisplayFeature;
+import androidx.window.extensions.ExtensionFoldingFeature;
import androidx.window.extensions.ExtensionWindowLayoutInfo;
import java.util.ArrayList;
@@ -37,9 +38,6 @@
DeviceState translate(ExtensionDeviceState deviceState) {
final int posture;
switch (deviceState.getPosture()) {
- case ExtensionDeviceState.POSTURE_CLOSED:
- posture = DeviceState.POSTURE_CLOSED;
- break;
case ExtensionDeviceState.POSTURE_FLIPPED:
posture = DeviceState.POSTURE_FLIPPED;
break;
@@ -49,7 +47,6 @@
case ExtensionDeviceState.POSTURE_OPENED:
posture = DeviceState.POSTURE_OPENED;
break;
- case ExtensionDeviceState.POSTURE_UNKNOWN:
default:
posture = DeviceState.POSTURE_UNKNOWN;
}
@@ -79,39 +76,62 @@
}
@Nullable
- DisplayFeature translate(Activity activity, ExtensionDisplayFeature feature) {
- final Rect windowBounds = WindowBoundsHelper.getInstance()
- .computeCurrentWindowBounds(activity);
- if (!isValid(feature, windowBounds)) {
+ DisplayFeature translate(Activity activity, ExtensionDisplayFeature displayFeature) {
+ if (!(displayFeature instanceof ExtensionFoldingFeature)) {
return null;
}
- int type = DisplayFeature.TYPE_FOLD;
- switch (feature.getType()) {
- case ExtensionDisplayFeature.TYPE_FOLD:
- type = DisplayFeature.TYPE_FOLD;
- break;
- case ExtensionDisplayFeature.TYPE_HINGE:
- type = DisplayFeature.TYPE_HINGE;
- break;
- }
- return new DisplayFeature(feature.getBounds(), type);
+ ExtensionFoldingFeature feature = (ExtensionFoldingFeature) displayFeature;
+ final Rect windowBounds = WindowBoundsHelper.getInstance()
+ .computeCurrentWindowBounds(activity);
+ return translateFoldFeature(windowBounds, feature);
}
- boolean isValid(ExtensionDisplayFeature feature, Rect windowBounds) {
+ @Nullable
+ private static DisplayFeature translateFoldFeature(@NonNull Rect windowBounds,
+ @NonNull ExtensionFoldingFeature feature) {
+ if (!isValid(windowBounds, feature)) {
+ return null;
+ }
+ int type = FoldingFeature.TYPE_FOLD;
+ switch (feature.getType()) {
+ case ExtensionFoldingFeature.TYPE_FOLD:
+ type = FoldingFeature.TYPE_FOLD;
+ break;
+ case ExtensionFoldingFeature.TYPE_HINGE:
+ type = FoldingFeature.TYPE_HINGE;
+ break;
+ }
+ int state = FoldingFeature.STATE_FLAT;
+ switch (feature.getState()) {
+ case ExtensionFoldingFeature.STATE_FLAT:
+ state = FoldingFeature.STATE_FLAT;
+ break;
+ case ExtensionFoldingFeature.STATE_FLIPPED:
+ state = FoldingFeature.STATE_FLIPPED;
+ break;
+ case ExtensionFoldingFeature.STATE_HALF_OPENED:
+ state = FoldingFeature.STATE_HALF_OPENED;
+ break;
+ }
+ return new FoldingFeature(feature.getBounds(), type, state);
+ }
+
+ private static boolean isValid(Rect windowBounds, ExtensionFoldingFeature feature) {
if (feature.getBounds().width() == 0 && feature.getBounds().height() == 0) {
return false;
}
- if (feature.getType() == ExtensionDisplayFeature.TYPE_FOLD
+ if (feature.getType() == ExtensionFoldingFeature.TYPE_FOLD
&& !feature.getBounds().isEmpty()) {
return false;
}
- if (!hasMatchingDimension(feature.getBounds(), windowBounds)) {
+ if (feature.getType() != ExtensionFoldingFeature.TYPE_FOLD
+ && feature.getType() != ExtensionFoldingFeature.TYPE_HINGE) {
return false;
}
- return true;
+ return hasMatchingDimension(feature.getBounds(), windowBounds);
}
- private boolean hasMatchingDimension(Rect lhs, Rect rhs) {
+ private static boolean hasMatchingDimension(Rect lhs, Rect rhs) {
boolean matchesWidth = lhs.left == rhs.left && lhs.right == rhs.right;
boolean matchesHeight = lhs.top == rhs.top && lhs.bottom == rhs.bottom;
return matchesWidth || matchesHeight;
diff --git a/window/window/src/main/java/androidx/window/ExtensionCompat.java b/window/window/src/main/java/androidx/window/ExtensionCompat.java
index 8a09172..ca97067 100644
--- a/window/window/src/main/java/androidx/window/ExtensionCompat.java
+++ b/window/window/src/main/java/androidx/window/ExtensionCompat.java
@@ -25,8 +25,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import androidx.window.extensions.ExtensionDeviceState;
import androidx.window.extensions.ExtensionDisplayFeature;
+import androidx.window.extensions.ExtensionFoldingFeature;
import androidx.window.extensions.ExtensionInterface;
import androidx.window.extensions.ExtensionProvider;
import androidx.window.extensions.ExtensionWindowLayoutInfo;
@@ -137,23 +137,20 @@
+ rtUnregisterWindowLayoutChangeListener);
}
- // ExtensionDeviceState constructor
- ExtensionDeviceState deviceState = new ExtensionDeviceState(
- ExtensionDeviceState.POSTURE_UNKNOWN);
+ // {@link ExtensionFoldingFeature} constructor
+ ExtensionFoldingFeature displayFoldingFeature =
+ new ExtensionFoldingFeature(new Rect(0, 0, 100, 0),
+ ExtensionFoldingFeature.TYPE_FOLD,
+ ExtensionFoldingFeature.STATE_FLAT);
- // deviceState.getPosture();
- int tmpPosture = deviceState.getPosture();
+ // displayFoldFeature.getBounds()
+ Rect tmpRect = displayFoldingFeature.getBounds();
- // ExtensionDisplayFeature constructor
- ExtensionDisplayFeature displayFeature =
- new ExtensionDisplayFeature(new Rect(0, 0, 0, 0),
- ExtensionDisplayFeature.TYPE_FOLD);
+ // displayFoldFeature.getState()
+ int tmpState = displayFoldingFeature.getState();
- // displayFeature.getBounds()
- Rect tmpRect = displayFeature.getBounds();
-
- // displayFeature.getType()
- int tmpType = displayFeature.getType();
+ // displayFoldFeature.getType()
+ int tmpType = displayFoldingFeature.getType();
// ExtensionWindowLayoutInfo constructor
ExtensionWindowLayoutInfo windowLayoutInfo =
@@ -163,10 +160,10 @@
List<ExtensionDisplayFeature> tmpDisplayFeatures =
windowLayoutInfo.getDisplayFeatures();
return true;
- } catch (Exception e) {
+ } catch (Throwable t) {
if (DEBUG) {
Log.e(TAG, "Extension implementation doesn't conform to interface version "
- + getExtensionVersion() + ", error: " + e);
+ + getExtensionVersion() + ", error: " + t);
}
return false;
}
diff --git a/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java b/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java
index 44e0c47..6488073 100644
--- a/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java
+++ b/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java
@@ -77,8 +77,12 @@
private static final String TAG = "WindowServer";
- private ExtensionWindowBackend() {
- // Empty
+ @VisibleForTesting
+ ExtensionWindowBackend(@Nullable ExtensionInterfaceCompat windowExtension) {
+ mWindowExtension = windowExtension;
+ if (mWindowExtension != null) {
+ mWindowExtension.setExtensionCallback(new ExtensionListenerImpl());
+ }
}
/**
@@ -89,25 +93,14 @@
if (sInstance == null) {
synchronized (sLock) {
if (sInstance == null) {
- sInstance = new ExtensionWindowBackend();
- sInstance.initExtension(context.getApplicationContext());
+ ExtensionInterfaceCompat windowExtension = initAndVerifyExtension(context);
+ sInstance = new ExtensionWindowBackend(windowExtension);
}
}
}
return sInstance;
}
- /** Tries to initialize Extension, returns early if it's not available. */
- @SuppressLint("SyntheticAccessor")
- @GuardedBy("sLock")
- private void initExtension(Context context) {
- mWindowExtension = initAndVerifyExtension(context);
- if (mWindowExtension == null) {
- return;
- }
- mWindowExtension.setExtensionCallback(new ExtensionListenerImpl());
- }
-
@Override
public void registerLayoutChangeCallback(@NonNull Activity activity,
@NonNull Executor executor, @NonNull Consumer<WindowLayoutInfo> callback) {
@@ -126,10 +119,12 @@
WindowLayoutChangeCallbackWrapper callbackWrapper =
new WindowLayoutChangeCallbackWrapper(activity, executor, callback);
mWindowLayoutChangeCallbacks.add(callbackWrapper);
+ // Read value before registering in case the extension updates synchronously.
+ // A synchronous update would result in two values emitted.
+ WindowLayoutInfo lastReportedValue = mLastReportedWindowLayouts.get(activity);
if (!isActivityRegistered) {
mWindowExtension.onWindowLayoutChangeListenerAdded(activity);
}
- WindowLayoutInfo lastReportedValue = mLastReportedWindowLayouts.get(activity);
if (lastReportedValue != null) {
callbackWrapper.accept(lastReportedValue);
}
diff --git a/window/window/src/main/java/androidx/window/FoldingFeature.java b/window/window/src/main/java/androidx/window/FoldingFeature.java
new file mode 100644
index 0000000..34f2b68
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/FoldingFeature.java
@@ -0,0 +1,201 @@
+/*
+ * 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;
+
+import android.graphics.Rect;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A feature that describes a fold in the flexible display
+ * or a hinge between two physical display panels.
+ */
+public class FoldingFeature implements DisplayFeature {
+
+ /**
+ * A fold in the flexible screen without a physical gap.
+ */
+ public static final int TYPE_FOLD = 1;
+
+ /**
+ * A physical separation with a hinge that allows two display panels to fold.
+ */
+ public static final int TYPE_HINGE = 2;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ TYPE_FOLD,
+ TYPE_HINGE,
+ })
+ @interface Type{}
+
+ /**
+ * The foldable device is completely open, the screen space that is presented to the user is
+ * flat. See the
+ * <a href="https://developer.android.com/guide/topics/ui/foldables#postures">Posture</a>
+ * section in the official documentation for visual samples and references.
+ */
+ public static final int STATE_FLAT = 1;
+
+ /**
+ * The foldable device's hinge is in an intermediate position between opened and closed state,
+ * there is a non-flat angle between parts of the flexible screen or between physical screen
+ * panels. See the
+ * <a href="https://developer.android.com/guide/topics/ui/foldables#postures">Posture</a>
+ * section in the official documentation for visual samples and references.
+ */
+ public static final int STATE_HALF_OPENED = 2;
+
+ /**
+ * The foldable device is flipped with the flexible screen parts or physical screens facing
+ * opposite directions. See the
+ * <a href="https://developer.android.com/guide/topics/ui/foldables#postures">Posture</a>
+ * section in the official documentation for visual samples and references.
+ */
+ public static final int STATE_FLIPPED = 3;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ STATE_HALF_OPENED,
+ STATE_FLAT,
+ STATE_FLIPPED,
+ })
+ @interface State {}
+
+ /**
+ * The bounding rectangle of the feature within the application window in the window
+ * coordinate space.
+ */
+ @NonNull
+ private final Rect mBounds;
+
+ /**
+ * The physical type of the feature.
+ */
+ @Type
+ private final int mType;
+
+ /**
+ * The state of the feature.
+ */
+ @State
+ private final int mState;
+
+ public FoldingFeature(@NonNull Rect bounds, @Type int type, @State int state) {
+ validateFeatureBounds(bounds, type);
+ mBounds = new Rect(bounds);
+ mType = type;
+ mState = state;
+ }
+
+ @NonNull
+ @Override
+ public Rect getBounds() {
+ return new Rect(mBounds);
+ }
+
+ @Type
+ public int getType() {
+ return mType;
+ }
+
+ @State
+ public int getState() {
+ return mState;
+ }
+
+ /**
+ * Verifies the bounds of the folding feature.
+ */
+ private static void validateFeatureBounds(@NonNull Rect bounds, int type) {
+ if (bounds.width() == 0 && bounds.height() == 0) {
+ throw new IllegalArgumentException("Bounds must be non zero");
+ }
+ if (type == TYPE_FOLD) {
+ if (bounds.width() != 0 && bounds.height() != 0) {
+ throw new IllegalArgumentException("Bounding rectangle must be either zero-wide "
+ + "or zero-high for features of type " + typeToString(type));
+ }
+
+ if ((bounds.width() != 0 && bounds.left != 0)
+ || (bounds.height() != 0 && bounds.top != 0)) {
+ throw new IllegalArgumentException("Bounding rectangle must span the entire "
+ + "window space for features of type " + typeToString(type));
+ }
+ } else if (type == TYPE_HINGE) {
+ if (bounds.left != 0 && bounds.top != 0) {
+ throw new IllegalArgumentException("Bounding rectangle must span the entire "
+ + "window space for features of type " + typeToString(type));
+ }
+ }
+ }
+
+ @NonNull
+ private static String typeToString(int type) {
+ switch (type) {
+ case TYPE_FOLD:
+ return "FOLD";
+ case TYPE_HINGE:
+ return "HINGE";
+ default:
+ return "Unknown feature type (" + type + ")";
+ }
+ }
+
+ @NonNull
+ private static String stateToString(int state) {
+ switch (state) {
+ case STATE_FLAT:
+ return "FLAT";
+ case STATE_FLIPPED:
+ return "FLIPPED";
+ case STATE_HALF_OPENED:
+ return "HALF_OPENED";
+ default:
+ return "Unknown feature state (" + state + ")";
+ }
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return FoldingFeature.class.getSimpleName() + " { " + mBounds + ", type="
+ + typeToString(getType()) + ", state=" + stateToString(mState) + " }";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof FoldingFeature)) return false;
+ FoldingFeature that = (FoldingFeature) o;
+ return mType == that.mType
+ && mState == that.mState
+ && mBounds.equals(that.mBounds);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mBounds.hashCode();
+ result = 31 * result + mType;
+ result = 31 * result + mState;
+ return result;
+ }
+}
diff --git a/window/window/src/main/java/androidx/window/SidecarAdapter.java b/window/window/src/main/java/androidx/window/SidecarAdapter.java
index 1a5ceb2..b7dcbdc 100644
--- a/window/window/src/main/java/androidx/window/SidecarAdapter.java
+++ b/window/window/src/main/java/androidx/window/SidecarAdapter.java
@@ -20,16 +20,20 @@
import static androidx.window.DeviceState.POSTURE_UNKNOWN;
import static androidx.window.ExtensionCompat.DEBUG;
+import android.annotation.SuppressLint;
import android.app.Activity;
import android.graphics.Rect;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import androidx.window.sidecar.SidecarDeviceState;
import androidx.window.sidecar.SidecarDisplayFeature;
import androidx.window.sidecar.SidecarWindowLayoutInfo;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
@@ -42,14 +46,17 @@
@NonNull
List<DisplayFeature> translate(SidecarWindowLayoutInfo sidecarWindowLayoutInfo,
- Rect windowBounds) {
+ SidecarDeviceState deviceState, Rect windowBounds) {
List<DisplayFeature> displayFeatures = new ArrayList<>();
- if (sidecarWindowLayoutInfo.displayFeatures == null) {
+ List<SidecarDisplayFeature> sidecarDisplayFeatures =
+ getSidecarDisplayFeatures(sidecarWindowLayoutInfo);
+ if (sidecarDisplayFeatures == null) {
return displayFeatures;
}
- for (SidecarDisplayFeature sidecarFeature : sidecarWindowLayoutInfo.displayFeatures) {
- final DisplayFeature displayFeature = translate(sidecarFeature, windowBounds);
+ for (SidecarDisplayFeature sidecarFeature : sidecarDisplayFeatures) {
+ final DisplayFeature displayFeature = translate(sidecarFeature, deviceState,
+ windowBounds);
if (displayFeature != null) {
displayFeatures.add(displayFeature);
}
@@ -57,32 +64,89 @@
return displayFeatures;
}
+ // TODO(b/172620880): Workaround for Sidecar API implementation issue.
+ @SuppressLint("BanUncheckedReflection")
+ @SuppressWarnings("unchecked")
+ @VisibleForTesting
+ @Nullable
+ static List<SidecarDisplayFeature> getSidecarDisplayFeatures(SidecarWindowLayoutInfo info) {
+ try {
+ return info.displayFeatures;
+ } catch (NoSuchFieldError error) {
+ try {
+ Method methodGetFeatures = SidecarWindowLayoutInfo.class.getMethod(
+ "getDisplayFeatures");
+ return (List<SidecarDisplayFeature>) methodGetFeatures.invoke(info);
+ } catch (NoSuchMethodException e) {
+ if (DEBUG) {
+ e.printStackTrace();
+ }
+ } catch (IllegalAccessException e) {
+ if (DEBUG) {
+ e.printStackTrace();
+ }
+ } catch (InvocationTargetException e) {
+ if (DEBUG) {
+ e.printStackTrace();
+ }
+ }
+ }
+ return null;
+ }
+
+ // TODO(b/172620880): Workaround for Sidecar API implementation issue.
+ @SuppressLint("BanUncheckedReflection")
+ @VisibleForTesting
+ static void setSidecarDisplayFeatures(SidecarWindowLayoutInfo info,
+ List<SidecarDisplayFeature> displayFeatures) {
+ try {
+ info.displayFeatures = displayFeatures;
+ } catch (NoSuchFieldError error) {
+ try {
+ Method methodSetFeatures = SidecarWindowLayoutInfo.class.getMethod(
+ "setDisplayFeatures", List.class);
+ methodSetFeatures.invoke(info, displayFeatures);
+ } catch (NoSuchMethodException e) {
+ if (DEBUG) {
+ e.printStackTrace();
+ }
+ } catch (IllegalAccessException e) {
+ if (DEBUG) {
+ e.printStackTrace();
+ }
+ } catch (InvocationTargetException e) {
+ if (DEBUG) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
@NonNull
WindowLayoutInfo translate(@NonNull Activity activity,
- @Nullable SidecarWindowLayoutInfo extensionInfo) {
+ @Nullable SidecarWindowLayoutInfo extensionInfo, @NonNull SidecarDeviceState state) {
if (extensionInfo == null) {
return new WindowLayoutInfo(new ArrayList<>());
}
+ SidecarDeviceState deviceState = new SidecarDeviceState();
+ int devicePosture = getSidecarDevicePosture(state);
+ setSidecarDevicePosture(deviceState, devicePosture);
+
Rect windowBounds = WindowBoundsHelper.getInstance().computeCurrentWindowBounds(activity);
- List<DisplayFeature> displayFeatures = translate(extensionInfo, windowBounds);
+ List<DisplayFeature> displayFeatures = translate(extensionInfo, deviceState, windowBounds);
return new WindowLayoutInfo(displayFeatures);
}
@NonNull
- DeviceState translate(@Nullable SidecarDeviceState sidecarDeviceState) {
- if (sidecarDeviceState == null) {
- return new DeviceState(POSTURE_UNKNOWN);
- }
-
+ DeviceState translate(@NonNull SidecarDeviceState sidecarDeviceState) {
int posture = postureFromSidecar(sidecarDeviceState);
return new DeviceState(posture);
}
-
@DeviceState.Posture
private static int postureFromSidecar(SidecarDeviceState sidecarDeviceState) {
- int sidecarPosture = sidecarDeviceState.posture;
+ int sidecarPosture = getSidecarDevicePosture(sidecarDeviceState);
if (sidecarPosture > POSTURE_MAX_KNOWN) {
if (DEBUG) {
Log.d(TAG, "Unknown posture reported, WindowManager library should be updated");
@@ -92,12 +156,67 @@
return sidecarPosture;
}
+ // TODO(b/172620880): Workaround for Sidecar API implementation issue.
+ @SuppressLint("BanUncheckedReflection")
+ @VisibleForTesting
+ static int getSidecarDevicePosture(SidecarDeviceState sidecarDeviceState) {
+ try {
+ return sidecarDeviceState.posture;
+ } catch (NoSuchFieldError error) {
+ try {
+ Method methodGetPosture = SidecarDeviceState.class.getMethod("getPosture");
+ return (int) methodGetPosture.invoke(sidecarDeviceState);
+ } catch (NoSuchMethodException e) {
+ if (DEBUG) {
+ e.printStackTrace();
+ }
+ } catch (IllegalAccessException e) {
+ if (DEBUG) {
+ e.printStackTrace();
+ }
+ } catch (InvocationTargetException e) {
+ if (DEBUG) {
+ e.printStackTrace();
+ }
+ }
+ }
+ return SidecarDeviceState.POSTURE_UNKNOWN;
+ }
+
+ // TODO(b/172620880): Workaround for Sidecar API implementation issue.
+ @SuppressLint("BanUncheckedReflection")
+ @VisibleForTesting
+ static void setSidecarDevicePosture(SidecarDeviceState sidecarDeviceState, int posture) {
+ try {
+ sidecarDeviceState.posture = posture;
+ } catch (NoSuchFieldError error) {
+ try {
+ Method methodSetPosture = SidecarDeviceState.class.getMethod("setPosture",
+ int.class);
+ methodSetPosture.invoke(sidecarDeviceState, posture);
+ } catch (NoSuchMethodException e) {
+ if (DEBUG) {
+ e.printStackTrace();
+ }
+ } catch (IllegalAccessException e) {
+ if (DEBUG) {
+ e.printStackTrace();
+ }
+ } catch (InvocationTargetException e) {
+ if (DEBUG) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
/**
* Converts the display feature from extension. Can return {@code null} if there is an issue
* with the value passed from extension.
*/
@Nullable
- private static DisplayFeature translate(SidecarDisplayFeature feature, Rect windowBounds) {
+ private static DisplayFeature translate(SidecarDisplayFeature feature,
+ SidecarDeviceState deviceState, Rect windowBounds) {
Rect bounds = feature.getRect();
if (bounds.width() == 0 && bounds.height() == 0) {
if (DEBUG) {
@@ -131,6 +250,31 @@
}
}
- return new DisplayFeature(feature.getRect(), feature.getType());
+ final int type;
+ if (feature.getType() == SidecarDisplayFeature.TYPE_HINGE) {
+ type = FoldingFeature.TYPE_HINGE;
+ } else {
+ type = FoldingFeature.TYPE_FOLD;
+ }
+
+ final int state;
+ final int devicePosture = getSidecarDevicePosture(deviceState);
+ switch (devicePosture) {
+ case SidecarDeviceState.POSTURE_CLOSED:
+ case SidecarDeviceState.POSTURE_UNKNOWN:
+ return null;
+ case SidecarDeviceState.POSTURE_FLIPPED:
+ state = FoldingFeature.STATE_FLIPPED;
+ break;
+ case SidecarDeviceState.POSTURE_HALF_OPENED:
+ state = FoldingFeature.STATE_HALF_OPENED;
+ break;
+ case SidecarDeviceState.POSTURE_OPENED:
+ default:
+ state = FoldingFeature.STATE_FLAT;
+ break;
+ }
+
+ return new FoldingFeature(feature.getRect(), type, state);
}
}
diff --git a/window/window/src/main/java/androidx/window/SidecarCompat.java b/window/window/src/main/java/androidx/window/SidecarCompat.java
index 7de2b77..295151b 100644
--- a/window/window/src/main/java/androidx/window/SidecarCompat.java
+++ b/window/window/src/main/java/androidx/window/SidecarCompat.java
@@ -32,7 +32,6 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.collection.SimpleArrayMap;
-import androidx.core.util.Consumer;
import androidx.window.sidecar.SidecarDeviceState;
import androidx.window.sidecar.SidecarDisplayFeature;
import androidx.window.sidecar.SidecarInterface;
@@ -40,6 +39,7 @@
import androidx.window.sidecar.SidecarWindowLayoutInfo;
import java.lang.reflect.Method;
+import java.util.ArrayList;
import java.util.List;
/** Extension interface compatibility wrapper for v0.1 sidecar. */
@@ -53,7 +53,7 @@
new SimpleArrayMap<>();
private ExtensionCallbackInterface mExtensionCallback;
- private SidecarAdapter mSidecarAdapter;
+ private final SidecarAdapter mSidecarAdapter;
@VisibleForTesting
final SidecarInterface mSidecar;
@@ -92,6 +92,17 @@
@SuppressLint("SyntheticAccessor")
public void onDeviceStateChanged(@NonNull SidecarDeviceState newDeviceState) {
extensionCallback.onDeviceStateChanged(mSidecarAdapter.translate(newDeviceState));
+
+ for (int i = 0; i < mWindowListenerRegisteredContexts.size(); i++) {
+ Activity activity = mWindowListenerRegisteredContexts.valueAt(i);
+ IBinder windowToken = getActivityWindowToken(activity);
+ if (windowToken == null) {
+ continue;
+ }
+ SidecarWindowLayoutInfo layoutInfo = mSidecar.getWindowLayoutInfo(windowToken);
+ extensionCallback.onWindowLayoutChanged(activity,
+ mSidecarAdapter.translate(activity, layoutInfo, newDeviceState));
+ }
}
@Override
@@ -106,7 +117,7 @@
}
extensionCallback.onWindowLayoutChanged(activity,
- mSidecarAdapter.translate(activity, newLayout));
+ mSidecarAdapter.translate(activity, newLayout, mSidecar.getDeviceState()));
}
});
}
@@ -117,7 +128,7 @@
IBinder windowToken = getActivityWindowToken(activity);
SidecarWindowLayoutInfo windowLayoutInfo = mSidecar.getWindowLayoutInfo(windowToken);
- return mSidecarAdapter.translate(activity, windowLayoutInfo);
+ return mSidecarAdapter.translate(activity, windowLayoutInfo, mSidecar.getDeviceState());
}
@Override
@@ -127,7 +138,8 @@
if (windowToken != null) {
register(windowToken, activity);
} else {
- FirstAttachAdapter attachAdapter = new FirstAttachAdapter((token) -> {
+ FirstAttachAdapter attachAdapter = new FirstAttachAdapter(() -> {
+ IBinder token = getActivityWindowToken(activity);
register(token, activity);
});
activity.getWindow().getDecorView().addOnAttachStateChangeListener(attachAdapter);
@@ -159,6 +171,7 @@
mSidecar.onDeviceStateListenersChanged(isEmpty);
}
+ @SuppressLint("BanUncheckedReflection")
@Override
@SuppressWarnings("unused")
public boolean validateExtensionInterface() {
@@ -215,7 +228,24 @@
tmpDeviceState = new SidecarDeviceState();
// deviceState.posture
- tmpDeviceState.posture = SidecarDeviceState.POSTURE_OPENED;
+ // TODO(b/172620880): Workaround for Sidecar API implementation issue.
+ try {
+ tmpDeviceState.posture = SidecarDeviceState.POSTURE_OPENED;
+ } catch (NoSuchFieldError error) {
+ if (DEBUG) {
+ Log.w(TAG, "Sidecar implementation doesn't conform to primary interface "
+ + "version, continue to check for the secondary one "
+ + VERSION_0_1 + ", error: " + error);
+ }
+ Method methodSetPosture = SidecarDeviceState.class.getMethod("setPosture",
+ int.class);
+ methodSetPosture.invoke(tmpDeviceState, SidecarDeviceState.POSTURE_OPENED);
+ Method methodGetPosture = SidecarDeviceState.class.getMethod("getPosture");
+ int posture = (int) methodGetPosture.invoke(tmpDeviceState);
+ if (posture != SidecarDeviceState.POSTURE_OPENED) {
+ throw new Exception("Invalid device posture getter/setter");
+ }
+ }
// SidecarDisplayFeature constructor
SidecarDisplayFeature displayFeature = new SidecarDisplayFeature();
@@ -232,13 +262,36 @@
SidecarWindowLayoutInfo windowLayoutInfo = new SidecarWindowLayoutInfo();
// windowLayoutInfo.displayFeatures
- final List<SidecarDisplayFeature> tmpDisplayFeatures = windowLayoutInfo.displayFeatures;
+ try {
+ final List<SidecarDisplayFeature> tmpDisplayFeatures =
+ windowLayoutInfo.displayFeatures;
+ // TODO(b/172620880): Workaround for Sidecar API implementation issue.
+ } catch (NoSuchFieldError error) {
+ if (DEBUG) {
+ Log.w(TAG, "Sidecar implementation doesn't conform to primary interface "
+ + "version, continue to check for the secondary one "
+ + VERSION_0_1 + ", error: " + error);
+ }
+ List<SidecarDisplayFeature> featureList = new ArrayList<>();
+ featureList.add(displayFeature);
+ Method methodSetFeatures = SidecarWindowLayoutInfo.class.getMethod(
+ "setDisplayFeatures", List.class);
+ methodSetFeatures.invoke(windowLayoutInfo, featureList);
+ Method methodGetFeatures = SidecarWindowLayoutInfo.class.getMethod(
+ "getDisplayFeatures");
+ @SuppressWarnings("unchecked")
+ final List<SidecarDisplayFeature> resultDisplayFeatures =
+ (List<SidecarDisplayFeature>) methodGetFeatures.invoke(windowLayoutInfo);
+ if (!featureList.equals(resultDisplayFeatures)) {
+ throw new Exception("Invalid display feature getter/setter");
+ }
+ }
return true;
- } catch (Exception e) {
+ } catch (Throwable t) {
if (DEBUG) {
- Log.e(TAG, "Extension implementation doesn't conform to interface version "
- + VERSION_0_1 + ", error: " + e);
+ Log.e(TAG, "Sidecar implementation doesn't conform to interface version "
+ + VERSION_0_1 + ", error: " + t);
}
return false;
}
@@ -273,15 +326,15 @@
*/
private static class FirstAttachAdapter implements View.OnAttachStateChangeListener {
- private final Consumer<IBinder> mCallback;
+ private final Runnable mCallback;
- FirstAttachAdapter(Consumer<IBinder> callback) {
+ FirstAttachAdapter(Runnable callback) {
mCallback = callback;
}
@Override
public void onViewAttachedToWindow(View view) {
- mCallback.accept(view.getWindowToken());
+ mCallback.run();
view.removeOnAttachStateChangeListener(this);
}
diff --git a/window/window/src/main/java/androidx/window/WindowManager.java b/window/window/src/main/java/androidx/window/WindowManager.java
index 6d83906..56d2988 100644
--- a/window/window/src/main/java/androidx/window/WindowManager.java
+++ b/window/window/src/main/java/androidx/window/WindowManager.java
@@ -50,6 +50,7 @@
* Gets an instance of the class initialized with and connected to the provided {@link Context}.
* All methods of this class will return information that is associated with this visual
* context.
+ *
* @param context A visual context, such as an {@link Activity} or a {@link ContextWrapper}
* around one, to use for initialization.
*/
@@ -61,8 +62,10 @@
* Gets an instance of the class initialized with and connected to the provided {@link Context}.
* All methods of this class will return information that is associated with this visual
* context.
- * @param context A visual context, such as an {@link Activity} or a {@link ContextWrapper}
- * around one, to use for initialization.
+ *
+ * @param context A visual context, such as an {@link Activity} or a
+ * {@link ContextWrapper}
+ * around one, to use for initialization.
* @param windowBackend Backing server class that will provide information for this instance.
* Pass a custom {@link WindowBackend} implementation for testing.
*/
@@ -80,6 +83,7 @@
/**
* Registers a callback for layout changes of the window of the current visual {@link Context}.
* Must be called only after the it is attached to the window.
+ *
* @see Activity#onAttachedToWindow()
*/
public void registerLayoutChangeCallback(@NonNull Executor executor,
@@ -95,16 +99,20 @@
}
/**
+ * @deprecated {@link DeviceState} information has been merged into {@link WindowLayoutInfo}
* Registers a callback for device state changes.
*/
+ @Deprecated
public void registerDeviceStateChangeCallback(@NonNull Executor executor,
@NonNull Consumer<DeviceState> callback) {
mWindowBackend.registerDeviceStateChangeCallback(executor, callback);
}
/**
+ * @deprecated {@link DeviceState} information has been merged into {@link WindowLayoutInfo}
* Unregisters a callback for device state changes.
*/
+ @Deprecated
public void unregisterDeviceStateChangeCallback(@NonNull Consumer<DeviceState> callback) {
mWindowBackend.unregisterDeviceStateChangeCallback(callback);
}
@@ -160,6 +168,7 @@
/**
* Unwraps the hierarchy of {@link ContextWrapper}-s until {@link Activity} is reached.
+ *
* @return Base {@link Activity} context or {@code null} if not available.
*/
@Nullable
diff --git a/car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl b/window/window/src/test/java/androidx/window/ActivityTestUtil.java
similarity index 65%
copy from car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl
copy to window/window/src/test/java/androidx/window/ActivityTestUtil.java
index 2896a83..2d6d5d7 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/IOnSelectedListener.aidl
+++ b/window/window/src/test/java/androidx/window/ActivityTestUtil.java
@@ -14,11 +14,18 @@
* limitations under the License.
*/
-package androidx.car.app;
+package androidx.window;
-import androidx.car.app.IOnDoneCallback;
+import android.app.Activity;
+import android.os.IBinder;
-/** @hide */
-oneway interface IOnSelectedListener {
- void onSelected(int index, IOnDoneCallback callback) = 1;
+import androidx.annotation.NonNull;
+
+public class ActivityTestUtil {
+
+ private ActivityTestUtil() { }
+
+ static IBinder getActivityWindowToken(@NonNull Activity activity) {
+ return activity.getWindow().getAttributes().token;
+ }
}
diff --git a/window/window/src/test/java/androidx/window/ExtensionWindowBackendUnitTest.java b/window/window/src/test/java/androidx/window/ExtensionWindowBackendUnitTest.java
new file mode 100644
index 0000000..b79864f
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/ExtensionWindowBackendUnitTest.java
@@ -0,0 +1,367 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.Activity;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
+
+import com.google.common.collect.BoundType;
+import com.google.common.collect.Range;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Unit tests for {@link ExtensionWindowBackend} that run on the JVM.
+ */
+@SuppressWarnings("deprecation") // TODO(b/173739071) Remove DeviceState
+public class ExtensionWindowBackendUnitTest {
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ mContext = mock(Context.class);
+ ExtensionWindowBackend.resetInstance();
+ }
+
+ @Test
+ public void testGetInstance() {
+ ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+ assertNotNull(backend);
+
+ // Verify that getInstance always returns the same value
+ ExtensionWindowBackend newBackend = ExtensionWindowBackend.getInstance(mContext);
+ assertEquals(backend, newBackend);
+ }
+
+ @Test
+ public void testRegisterDeviceStateChangeCallback_noExtension() {
+ // Verify method with extension
+ ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+ assumeTrue(backend.mWindowExtension == null);
+ SimpleConsumer<DeviceState> simpleConsumer = new SimpleConsumer<>();
+
+ backend.registerDeviceStateChangeCallback(directExecutor(), simpleConsumer);
+
+ DeviceState deviceState = simpleConsumer.lastValue();
+ assertNotNull(deviceState);
+ assertThat(deviceState.getPosture()).isIn(Range.range(
+ DeviceState.POSTURE_UNKNOWN, BoundType.CLOSED,
+ DeviceState.POSTURE_MAX_KNOWN, BoundType.CLOSED));
+ DeviceState initialLastReportedState = backend.mLastReportedDeviceState;
+
+ // Verify method without extension
+ backend.mWindowExtension = null;
+ SimpleConsumer<DeviceState> noExtensionConsumer = new SimpleConsumer<>();
+ backend.registerDeviceStateChangeCallback(directExecutor(), noExtensionConsumer);
+ deviceState = noExtensionConsumer.lastValue();
+ assertNotNull(deviceState);
+ assertEquals(DeviceState.POSTURE_UNKNOWN, deviceState.getPosture());
+ // Verify that last reported state does not change when using the getter
+ assertEquals(initialLastReportedState, backend.mLastReportedDeviceState);
+ }
+
+ @Test
+ public void testRegisterLayoutChangeCallback() {
+ ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+ backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
+
+ // Check registering the layout change callback
+ Consumer<WindowLayoutInfo> consumer = mock(WindowLayoutInfoConsumer.class);
+ Activity activity = mock(Activity.class);
+ backend.registerLayoutChangeCallback(activity, Runnable::run, consumer);
+
+ assertEquals(1, backend.mWindowLayoutChangeCallbacks.size());
+ verify(backend.mWindowExtension).onWindowLayoutChangeListenerAdded(activity);
+
+ // Check unregistering the layout change callback
+ backend.unregisterLayoutChangeCallback(consumer);
+
+ assertTrue(backend.mWindowLayoutChangeCallbacks.isEmpty());
+ verify(backend.mWindowExtension).onWindowLayoutChangeListenerRemoved(eq(activity));
+ }
+
+ @Test
+ public void testRegisterLayoutChangeCallback_synchronousExtension() {
+ WindowLayoutInfo expectedInfo = newTestWindowLayoutInfo();
+ ExtensionInterfaceCompat extensionInterfaceCompat =
+ new SynchronousExtensionInterface(expectedInfo,
+ newTestDeviceState());
+ ExtensionWindowBackend backend = new ExtensionWindowBackend(extensionInterfaceCompat);
+
+ // Check registering the layout change callback
+ Consumer<WindowLayoutInfo> consumer = mock(WindowLayoutInfoConsumer.class);
+ Activity activity = mock(Activity.class);
+ backend.registerLayoutChangeCallback(activity, Runnable::run, consumer);
+
+ // Check unregistering the layout change callback
+ verify(consumer).accept(expectedInfo);
+ }
+
+ @Test
+ public void testRegisterLayoutChangeCallback_callsExtensionOnce() {
+ ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+ backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
+
+ // Check registering the layout change callback
+ Consumer<WindowLayoutInfo> consumer = mock(WindowLayoutInfoConsumer.class);
+ Activity activity = mock(Activity.class);
+ backend.registerLayoutChangeCallback(activity, Runnable::run, consumer);
+ backend.registerLayoutChangeCallback(activity, Runnable::run,
+ mock(WindowLayoutInfoConsumer.class));
+
+ assertEquals(2, backend.mWindowLayoutChangeCallbacks.size());
+ verify(backend.mWindowExtension).onWindowLayoutChangeListenerAdded(activity);
+
+ // Check unregistering the layout change callback
+ backend.unregisterLayoutChangeCallback(consumer);
+
+ assertEquals(1, backend.mWindowLayoutChangeCallbacks.size());
+ verify(backend.mWindowExtension, times(0))
+ .onWindowLayoutChangeListenerRemoved(eq(activity));
+ }
+
+ @Test
+ public void testRegisterLayoutChangeCallback_clearListeners() {
+ ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+ backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
+
+ // Check registering the layout change callback
+ Consumer<WindowLayoutInfo> firstConsumer = mock(WindowLayoutInfoConsumer.class);
+ Consumer<WindowLayoutInfo> secondConsumer = mock(WindowLayoutInfoConsumer.class);
+ Activity activity = mock(Activity.class);
+ backend.registerLayoutChangeCallback(activity, Runnable::run, firstConsumer);
+ backend.registerLayoutChangeCallback(activity, Runnable::run, secondConsumer);
+
+ // Check unregistering the layout change callback
+ backend.unregisterLayoutChangeCallback(firstConsumer);
+ backend.unregisterLayoutChangeCallback(secondConsumer);
+
+ assertTrue(backend.mWindowLayoutChangeCallbacks.isEmpty());
+ verify(backend.mWindowExtension).onWindowLayoutChangeListenerRemoved(activity);
+ }
+
+ @Test
+ public void testRegisterDeviceChangeCallback() {
+ ExtensionInterfaceCompat mockInterface = mock(ExtensionInterfaceCompat.class);
+ ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+ backend.mWindowExtension = mockInterface;
+
+ // Check registering the device state change callback
+ Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
+ backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
+
+ assertEquals(1, backend.mDeviceStateChangeCallbacks.size());
+ verify(backend.mWindowExtension).onDeviceStateListenersChanged(eq(false));
+
+ // Check unregistering the device state change callback
+ backend.unregisterDeviceStateChangeCallback(consumer);
+
+ assertTrue(backend.mDeviceStateChangeCallbacks.isEmpty());
+ verify(backend.mWindowExtension).onDeviceStateListenersChanged(eq(true));
+ }
+
+ @Test
+ public void testDeviceChangeCallback() {
+ ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+ backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
+
+ // Check that callbacks from the extension are propagated correctly
+ Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
+
+ backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
+ DeviceState deviceState = newTestDeviceState();
+ ExtensionWindowBackend.ExtensionListenerImpl backendListener =
+ backend.new ExtensionListenerImpl();
+ backendListener.onDeviceStateChanged(deviceState);
+
+ verify(consumer, times(1)).accept(eq(deviceState));
+ assertEquals(deviceState, backend.mLastReportedDeviceState);
+
+ // Test that the same value wouldn't be reported again
+ backendListener.onDeviceStateChanged(deviceState);
+ verify(consumer, times(1)).accept(any());
+ }
+
+ @Test
+ public void testDeviceChangeChangeCallback_callsExtensionOnce() {
+ ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+ backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
+
+ // Check registering the layout change callback
+ Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
+ backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
+ backend.registerDeviceStateChangeCallback(Runnable::run, mock(DeviceStateConsumer.class));
+
+ assertEquals(2, backend.mDeviceStateChangeCallbacks.size());
+ verify(backend.mWindowExtension).onDeviceStateListenersChanged(false);
+
+ // Check unregistering the layout change callback
+ backend.unregisterDeviceStateChangeCallback(consumer);
+
+ assertEquals(1, backend.mDeviceStateChangeCallbacks.size());
+ verify(backend.mWindowExtension, times(0))
+ .onDeviceStateListenersChanged(true);
+ }
+
+ @Test
+ public void testDeviceChangeChangeCallback_clearListeners() {
+ ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+ backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
+
+ // Check registering the layout change callback
+ Consumer<DeviceState> firstConsumer = mock(DeviceStateConsumer.class);
+ Consumer<DeviceState> secondConsumer = mock(DeviceStateConsumer.class);
+ backend.registerDeviceStateChangeCallback(Runnable::run, firstConsumer);
+ backend.registerDeviceStateChangeCallback(Runnable::run, secondConsumer);
+
+ // Check unregistering the layout change callback
+ backend.unregisterDeviceStateChangeCallback(firstConsumer);
+ backend.unregisterDeviceStateChangeCallback(secondConsumer);
+
+ assertTrue(backend.mDeviceStateChangeCallbacks.isEmpty());
+ verify(backend.mWindowExtension).onDeviceStateListenersChanged(true);
+ }
+
+ @Test
+ public void testDeviceChangeCallback_relayLastEmittedValue() {
+ DeviceState expectedState = newTestDeviceState();
+ ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+ Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
+ backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
+ backend.mLastReportedDeviceState = expectedState;
+
+ backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
+
+ verify(consumer).accept(expectedState);
+ }
+
+ @Test
+ public void testDeviceChangeCallback_clearLastEmittedValue() {
+ ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
+ Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
+
+ backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
+ backend.unregisterDeviceStateChangeCallback(consumer);
+
+ assertTrue(backend.mDeviceStateChangeCallbacks.isEmpty());
+ assertNull(backend.mLastReportedDeviceState);
+ }
+
+ private static WindowLayoutInfo newTestWindowLayoutInfo() {
+ WindowLayoutInfo.Builder builder = new WindowLayoutInfo.Builder();
+ return builder.build();
+ }
+
+ private static DeviceState newTestDeviceState() {
+ DeviceState.Builder builder = new DeviceState.Builder();
+ builder.setPosture(DeviceState.POSTURE_OPENED);
+ return builder.build();
+ }
+
+ private interface DeviceStateConsumer extends Consumer<DeviceState> { }
+
+ private interface WindowLayoutInfoConsumer extends Consumer<WindowLayoutInfo> { }
+
+ private static class SimpleConsumer<T> implements Consumer<T> {
+ private final List<T> mValues;
+
+ SimpleConsumer() {
+ mValues = new ArrayList<>();
+ }
+
+ @Override
+ public void accept(T t) {
+ mValues.add(t);
+ }
+
+ T lastValue() {
+ return mValues.get(mValues.size() - 1);
+ }
+ }
+
+ private static class SynchronousExtensionInterface implements ExtensionInterfaceCompat {
+
+ private ExtensionCallbackInterface mInterface;
+ private final DeviceState mDeviceState;
+ private final WindowLayoutInfo mWindowLayoutInfo;
+
+ SynchronousExtensionInterface(WindowLayoutInfo windowLayoutInfo, DeviceState deviceState) {
+ mInterface = new ExtensionCallbackInterface() {
+ @Override
+ public void onDeviceStateChanged(@NonNull @NotNull DeviceState newDeviceState) {
+
+ }
+
+ @Override
+ public void onWindowLayoutChanged(@NonNull @NotNull Activity activity,
+ @NonNull @NotNull WindowLayoutInfo newLayout) {
+
+ }
+ };
+ mWindowLayoutInfo = windowLayoutInfo;
+ mDeviceState = deviceState;
+ }
+
+ @Override
+ public boolean validateExtensionInterface() {
+ return true;
+ }
+
+ @Override
+ public void setExtensionCallback(
+ @NonNull @NotNull ExtensionCallbackInterface extensionCallback) {
+ mInterface = extensionCallback;
+ }
+
+ @Override
+ public void onWindowLayoutChangeListenerAdded(@NonNull @NotNull Activity activity) {
+ mInterface.onWindowLayoutChanged(activity, mWindowLayoutInfo);
+ }
+
+ @Override
+ public void onWindowLayoutChangeListenerRemoved(@NonNull @NotNull Activity activity) {
+
+ }
+
+ @Override
+ public void onDeviceStateListenersChanged(boolean isEmpty) {
+ mInterface.onDeviceStateChanged(mDeviceState);
+ }
+ }
+}
diff --git a/window/window/src/test/java/androidx/window/SidecarCompatUnitTest.java b/window/window/src/test/java/androidx/window/SidecarCompatUnitTest.java
new file mode 100644
index 0000000..a7b9da2
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/SidecarCompatUnitTest.java
@@ -0,0 +1,376 @@
+/*
+ * 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;
+
+import static androidx.window.ActivityTestUtil.getActivityWindowToken;
+import static androidx.window.TestBoundsUtil.invalidFoldBounds;
+import static androidx.window.TestBoundsUtil.invalidHingeBounds;
+import static androidx.window.TestBoundsUtil.validFoldBound;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+import androidx.window.sidecar.SidecarDeviceState;
+import androidx.window.sidecar.SidecarDisplayFeature;
+import androidx.window.sidecar.SidecarInterface;
+import androidx.window.sidecar.SidecarWindowLayoutInfo;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Unit tests for {@link SidecarCompat} that run on the JVM.
+ */
+public final class SidecarCompatUnitTest {
+
+ private static final Rect WINDOW_BOUNDS = new Rect(1, 1, 50, 100);
+
+ private Activity mActivity;
+ private SidecarCompat mSidecarCompat;
+
+ @Before
+ public void setUp() {
+ TestWindowBoundsHelper mWindowBoundsHelper = new TestWindowBoundsHelper();
+ mWindowBoundsHelper.setCurrentBounds(WINDOW_BOUNDS);
+ WindowBoundsHelper.setForTesting(mWindowBoundsHelper);
+
+ mActivity = mock(Activity.class);
+ Window window = spy(new TestWindow(mActivity));
+ WindowManager.LayoutParams params = new WindowManager.LayoutParams();
+ doReturn(params).when(window).getAttributes();
+ when(mActivity.getWindow()).thenReturn(window);
+
+ SidecarInterface mockSidecarInterface = mock(SidecarInterface.class);
+ when(mockSidecarInterface.getDeviceState()).thenReturn(
+ newDeviceState(DeviceState.POSTURE_FLIPPED));
+ when(mockSidecarInterface.getWindowLayoutInfo(any())).thenReturn(
+ newWindowLayoutInfo(new ArrayList<>()));
+ mSidecarCompat = new SidecarCompat(mockSidecarInterface, new SidecarAdapter());
+ }
+
+ @After
+ public void tearDown() {
+ WindowBoundsHelper.setForTesting(null);
+ }
+
+ @Test
+ public void testGetDeviceState() {
+ FakeExtensionImp fakeSidecarImp = new FakeExtensionImp();
+ SidecarCompat compat = new SidecarCompat(fakeSidecarImp, new SidecarAdapter());
+ ExtensionInterfaceCompat.ExtensionCallbackInterface mockCallback = mock(
+ ExtensionInterfaceCompat.ExtensionCallbackInterface.class);
+ compat.setExtensionCallback(mockCallback);
+ compat.onDeviceStateListenersChanged(false);
+ SidecarDeviceState deviceState = newDeviceState(SidecarDeviceState.POSTURE_OPENED);
+
+ fakeSidecarImp.triggerDeviceState(deviceState);
+
+ verify(mockCallback).onDeviceStateChanged(any());
+ }
+
+ @Test
+ public void testGetWindowLayout_featureWithEmptyBounds() {
+ // Add a feature with an empty bounds to the reported list
+ SidecarWindowLayoutInfo originalWindowLayoutInfo =
+ mSidecarCompat.mSidecar.getWindowLayoutInfo(getActivityWindowToken(mActivity));
+ List<SidecarDisplayFeature> sidecarDisplayFeatures =
+ originalWindowLayoutInfo.displayFeatures;
+ SidecarDisplayFeature newFeature = new SidecarDisplayFeature();
+ newFeature.setRect(new Rect());
+ sidecarDisplayFeatures.add(newFeature);
+
+ // Verify that this feature is skipped.
+ WindowLayoutInfo windowLayoutInfo = mSidecarCompat.getWindowLayoutInfo(mActivity);
+
+ assertEquals(sidecarDisplayFeatures.size() - 1,
+ windowLayoutInfo.getDisplayFeatures().size());
+ }
+
+ @Test
+ public void testGetWindowLayout_foldWithNonZeroArea() {
+ SidecarWindowLayoutInfo originalWindowLayoutInfo =
+ mSidecarCompat.mSidecar.getWindowLayoutInfo(mock(IBinder.class));
+ List<SidecarDisplayFeature> sidecarDisplayFeatures =
+ originalWindowLayoutInfo.displayFeatures;
+ // Horizontal fold.
+ sidecarDisplayFeatures.add(
+ newDisplayFeature(new Rect(0, 1, WINDOW_BOUNDS.width(), 2),
+ SidecarDisplayFeature.TYPE_FOLD));
+ // Vertical fold.
+ sidecarDisplayFeatures.add(
+ newDisplayFeature(new Rect(1, 0, 2, WINDOW_BOUNDS.height()),
+ SidecarDisplayFeature.TYPE_FOLD));
+
+ // Verify that these features are skipped.
+ WindowLayoutInfo windowLayoutInfo =
+ mSidecarCompat.getWindowLayoutInfo(mActivity);
+
+ assertEquals(sidecarDisplayFeatures.size() - 2,
+ windowLayoutInfo.getDisplayFeatures().size());
+ }
+
+ @Test
+ public void testGetWindowLayout_hingeNotSpanningEntireWindow() {
+ SidecarWindowLayoutInfo originalWindowLayoutInfo =
+ mSidecarCompat.mSidecar.getWindowLayoutInfo(mock(IBinder.class));
+ List<SidecarDisplayFeature> sidecarDisplayFeatures =
+ originalWindowLayoutInfo.displayFeatures;
+ // Horizontal hinge.
+ sidecarDisplayFeatures.add(
+ newDisplayFeature(new Rect(0, 1, WINDOW_BOUNDS.width() - 1, 2),
+ SidecarDisplayFeature.TYPE_FOLD));
+ // Vertical hinge.
+ sidecarDisplayFeatures.add(
+ newDisplayFeature(new Rect(1, 0, 2, WINDOW_BOUNDS.height() - 1),
+ SidecarDisplayFeature.TYPE_FOLD));
+
+ // Verify that these features are skipped.
+ WindowLayoutInfo windowLayoutInfo =
+ mSidecarCompat.getWindowLayoutInfo(mActivity);
+
+ assertEquals(sidecarDisplayFeatures.size() - 2,
+ windowLayoutInfo.getDisplayFeatures().size());
+ }
+
+ @Test
+ public void testGetWindowLayout_foldNotSpanningEntireWindow() {
+ SidecarWindowLayoutInfo originalWindowLayoutInfo =
+ mSidecarCompat.mSidecar.getWindowLayoutInfo(mock(IBinder.class));
+ List<SidecarDisplayFeature> sidecarDisplayFeatures =
+ originalWindowLayoutInfo.displayFeatures;
+ // Horizontal fold.
+ sidecarDisplayFeatures.add(
+ newDisplayFeature(new Rect(0, 1, WINDOW_BOUNDS.width() - 1, 2),
+ SidecarDisplayFeature.TYPE_FOLD));
+ // Vertical fold.
+ sidecarDisplayFeatures.add(
+ newDisplayFeature(new Rect(1, 0, 2, WINDOW_BOUNDS.height() - 1),
+ SidecarDisplayFeature.TYPE_FOLD));
+
+ // Verify that these features are skipped.
+ WindowLayoutInfo windowLayoutInfo =
+ mSidecarCompat.getWindowLayoutInfo(mActivity);
+
+ assertEquals(sidecarDisplayFeatures.size() - 2,
+ windowLayoutInfo.getDisplayFeatures().size());
+ }
+
+ @Test
+ public void testOnWindowLayoutChangeListenerAdded() {
+ IBinder expectedToken = mock(IBinder.class);
+ mActivity.getWindow().getAttributes().token = expectedToken;
+ mSidecarCompat.onWindowLayoutChangeListenerAdded(mActivity);
+ verify(mSidecarCompat.mSidecar).onWindowLayoutChangeListenerAdded(eq(expectedToken));
+ }
+
+ @Test
+ public void testOnWindowLayoutChangeListenerAdded_emitInitialValueDelayed() {
+ SidecarWindowLayoutInfo layoutInfo = new SidecarWindowLayoutInfo();
+ WindowLayoutInfo expectedLayoutInfo = new WindowLayoutInfo(new ArrayList<>());
+ ExtensionInterfaceCompat.ExtensionCallbackInterface listener =
+ mock(ExtensionInterfaceCompat.ExtensionCallbackInterface.class);
+ mSidecarCompat.setExtensionCallback(listener);
+ when(mSidecarCompat.mSidecar.getWindowLayoutInfo(any())).thenReturn(layoutInfo);
+ View fakeView = mock(View.class);
+ Window mockWindow = mock(Window.class);
+ when(mockWindow.getAttributes()).thenReturn(new WindowManager.LayoutParams());
+ doAnswer(invocation -> {
+ View.OnAttachStateChangeListener stateChangeListener = invocation.getArgument(0);
+ mockWindow.getAttributes().token = mock(IBinder.class);
+ stateChangeListener.onViewAttachedToWindow(fakeView);
+ return null;
+ }).when(fakeView).addOnAttachStateChangeListener(any());
+ when(mockWindow.getDecorView()).thenReturn(fakeView);
+ when(mActivity.getWindow()).thenReturn(mockWindow);
+
+ mSidecarCompat.onWindowLayoutChangeListenerAdded(mActivity);
+
+ verify(listener).onWindowLayoutChanged(mActivity, expectedLayoutInfo);
+ verify(mSidecarCompat.mSidecar).onWindowLayoutChangeListenerAdded(
+ getActivityWindowToken(mActivity));
+ verify(fakeView).addOnAttachStateChangeListener(any());
+ }
+
+ @Test
+ public void testOnWindowLayoutChangeListenerRemoved() {
+ IBinder windowToken = getActivityWindowToken(mActivity);
+ mSidecarCompat.onWindowLayoutChangeListenerRemoved(mActivity);
+ verify(mSidecarCompat.mSidecar).onWindowLayoutChangeListenerRemoved(eq(windowToken));
+ }
+
+ @Test
+ public void testOnDeviceStateListenersChanged() {
+ mSidecarCompat.onDeviceStateListenersChanged(true);
+ verify(mSidecarCompat.mSidecar).onDeviceStateListenersChanged(eq(true));
+ }
+
+ @Test
+ public void testOnDeviceStateListenersAdded_emitInitialValue() {
+ SidecarDeviceState deviceState = new SidecarDeviceState();
+ DeviceState expectedDeviceState = new DeviceState(DeviceState.POSTURE_UNKNOWN);
+ ExtensionInterfaceCompat.ExtensionCallbackInterface listener =
+ mock(ExtensionInterfaceCompat.ExtensionCallbackInterface.class);
+ mSidecarCompat.setExtensionCallback(listener);
+ when(mSidecarCompat.mSidecar.getDeviceState()).thenReturn(deviceState);
+
+ mSidecarCompat.onDeviceStateListenersChanged(false);
+
+ verify(listener).onDeviceStateChanged(expectedDeviceState);
+ }
+
+ private static SidecarDisplayFeature newDisplayFeature(Rect rect, int type) {
+ SidecarDisplayFeature feature = new SidecarDisplayFeature();
+ feature.setRect(rect);
+ feature.setType(type);
+ return feature;
+ }
+
+ private static SidecarWindowLayoutInfo newWindowLayoutInfo(
+ List<SidecarDisplayFeature> features) {
+ SidecarWindowLayoutInfo info = new SidecarWindowLayoutInfo();
+ info.displayFeatures = new ArrayList<>();
+ info.displayFeatures.addAll(features);
+ return info;
+ }
+
+ private static SidecarDeviceState newDeviceState(int posture) {
+ SidecarDeviceState state = new SidecarDeviceState();
+ state.posture = posture;
+ return state;
+ }
+
+ private static final class FakeExtensionImp implements SidecarInterface {
+
+ private SidecarInterface.SidecarCallback mCallback;
+ private List<IBinder> mTokens = new ArrayList<>();
+
+ FakeExtensionImp() {
+ mCallback = new SidecarInterface.SidecarCallback() {
+ @Override
+ public void onDeviceStateChanged(@NonNull SidecarDeviceState newDeviceState) {
+
+ }
+
+ @Override
+ public void onWindowLayoutChanged(@NonNull IBinder windowToken,
+ @NonNull SidecarWindowLayoutInfo newLayout) {
+
+ }
+ };
+ }
+
+ @Override
+ public void setSidecarCallback(@NonNull SidecarCallback callback) {
+
+ }
+
+ @NonNull
+ @Override
+ public SidecarWindowLayoutInfo getWindowLayoutInfo(@NonNull IBinder windowToken) {
+ return null;
+ }
+
+ @Override
+ public void onWindowLayoutChangeListenerAdded(@NonNull IBinder windowToken) {
+
+ }
+
+ @Override
+ public void onWindowLayoutChangeListenerRemoved(@NonNull IBinder windowToken) {
+
+ }
+
+ @NonNull
+ @Override
+ public SidecarDeviceState getDeviceState() {
+ SidecarDeviceState state = new SidecarDeviceState();
+ return state;
+ }
+
+ @Override
+ public void onDeviceStateListenersChanged(boolean isEmpty) {
+
+ }
+
+ void triggerMalformedSignal() {
+ triggerSignal(malformedWindowLayoutInfo());
+ }
+
+ void triggerGoodSignal() {
+ triggerSignal(validWindowLayoutInfo());
+ }
+
+ void triggerSignal(SidecarWindowLayoutInfo info) {
+ for (IBinder token: mTokens) {
+ triggerSignal(token, info);
+ }
+ }
+
+ void triggerSignal(IBinder token, SidecarWindowLayoutInfo info) {
+ mCallback.onWindowLayoutChanged(token, info);
+ }
+
+ public void triggerDeviceState(SidecarDeviceState state) {
+ mCallback.onDeviceStateChanged(state);
+
+ }
+
+ private SidecarWindowLayoutInfo malformedWindowLayoutInfo() {
+ List<SidecarDisplayFeature> malformedFeatures = new ArrayList<>();
+
+ for (Rect malformedBound : invalidFoldBounds(WINDOW_BOUNDS)) {
+ malformedFeatures.add(newDisplayFeature(malformedBound,
+ SidecarDisplayFeature.TYPE_FOLD));
+ }
+
+ for (Rect malformedBound : invalidHingeBounds(WINDOW_BOUNDS)) {
+ malformedFeatures.add(newDisplayFeature(malformedBound,
+ SidecarDisplayFeature.TYPE_HINGE));
+ }
+
+ return newWindowLayoutInfo(malformedFeatures);
+ }
+
+ private SidecarWindowLayoutInfo validWindowLayoutInfo() {
+ List<SidecarDisplayFeature> goodFeatures = new ArrayList<>();
+
+ goodFeatures.add(newDisplayFeature(validFoldBound(WINDOW_BOUNDS),
+ SidecarDisplayFeature.TYPE_FOLD));
+
+ return newWindowLayoutInfo(goodFeatures);
+ }
+ }
+}
diff --git a/window/window/src/androidTest/java/androidx/window/TestBoundUtil.java b/window/window/src/test/java/androidx/window/TestBoundsUtil.java
similarity index 91%
rename from window/window/src/androidTest/java/androidx/window/TestBoundUtil.java
rename to window/window/src/test/java/androidx/window/TestBoundsUtil.java
index 18447f5..4988e40 100644
--- a/window/window/src/androidTest/java/androidx/window/TestBoundUtil.java
+++ b/window/window/src/test/java/androidx/window/TestBoundsUtil.java
@@ -24,7 +24,7 @@
/**
* A utility class to provide bounds for a display feature
*/
-class TestBoundUtil {
+class TestBoundsUtil {
public static Rect validFoldBound(Rect windowBounds) {
return new Rect(windowBounds.left, windowBounds.top, windowBounds.right, 0);
@@ -38,7 +38,7 @@
return new Rect(windowBounds.left, windowBounds.top, windowBounds.right / 2, 2);
}
- public static Rect invalidBoundShortHeightHeight(Rect windowBounds) {
+ public static Rect invalidBoundShortHeight(Rect windowBounds) {
return new Rect(windowBounds.left, windowBounds.top, 2, windowBounds.bottom / 2);
}
@@ -47,7 +47,7 @@
badBounds.add(invalidZeroBound());
badBounds.add(invalidBoundShortWidth(windowBounds));
- badBounds.add(invalidBoundShortHeightHeight(windowBounds));
+ badBounds.add(invalidBoundShortHeight(windowBounds));
return badBounds;
}
diff --git a/window/window/src/test/java/androidx/window/TestWindow.java b/window/window/src/test/java/androidx/window/TestWindow.java
new file mode 100644
index 0000000..8e3591f
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/TestWindow.java
@@ -0,0 +1,293 @@
+/*
+ * 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;
+
+import static org.mockito.Mockito.mock;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.InputQueue;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class TestWindow extends Window {
+
+ private View mDecorView;
+
+ public TestWindow(Context context) {
+ this(context, mock(View.class));
+ }
+
+ public TestWindow(Context context, View decorView) {
+ super(context);
+ mDecorView = decorView;
+ }
+
+ @Override
+ public void takeSurface(SurfaceHolder.Callback2 callback) {
+
+ }
+
+ @Override
+ public void takeInputQueue(InputQueue.Callback callback) {
+
+ }
+
+ @Override
+ public boolean isFloating() {
+ return false;
+ }
+
+ @Override
+ public void setContentView(int layoutResID) {
+
+ }
+
+ @Override
+ public void setContentView(View view) {
+
+ }
+
+ @Override
+ public void setContentView(View view, ViewGroup.LayoutParams params) {
+
+ }
+
+ @Override
+ public void addContentView(View view, ViewGroup.LayoutParams params) {
+
+ }
+
+ @Nullable
+ @Override
+ public View getCurrentFocus() {
+ return null;
+ }
+
+ @NonNull
+ @Override
+ public LayoutInflater getLayoutInflater() {
+ return null;
+ }
+
+ @Override
+ public void setTitle(CharSequence title) {
+
+ }
+
+ @Override
+ public void setTitleColor(int textColor) {
+
+ }
+
+ @Override
+ public void openPanel(int featureId, KeyEvent event) {
+
+ }
+
+ @Override
+ public void closePanel(int featureId) {
+
+ }
+
+ @Override
+ public void togglePanel(int featureId, KeyEvent event) {
+
+ }
+
+ @Override
+ public void invalidatePanelMenu(int featureId) {
+
+ }
+
+ @Override
+ public boolean performPanelShortcut(int featureId, int keyCode, KeyEvent event, int flags) {
+ return false;
+ }
+
+ @Override
+ public boolean performPanelIdentifierAction(int featureId, int id, int flags) {
+ return false;
+ }
+
+ @Override
+ public void closeAllPanels() {
+
+ }
+
+ @Override
+ public boolean performContextMenuIdentifierAction(int id, int flags) {
+ return false;
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+
+ }
+
+ @Override
+ public void setBackgroundDrawable(Drawable drawable) {
+
+ }
+
+ @Override
+ public void setFeatureDrawableResource(int featureId, int resId) {
+
+ }
+
+ @Override
+ public void setFeatureDrawableUri(int featureId, Uri uri) {
+
+ }
+
+ @Override
+ public void setFeatureDrawable(int featureId, Drawable drawable) {
+
+ }
+
+ @Override
+ public void setFeatureDrawableAlpha(int featureId, int alpha) {
+
+ }
+
+ @Override
+ public void setFeatureInt(int featureId, int value) {
+
+ }
+
+ @Override
+ public void takeKeyEvents(boolean get) {
+
+ }
+
+ @Override
+ public boolean superDispatchKeyEvent(KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean superDispatchKeyShortcutEvent(KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean superDispatchTouchEvent(MotionEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean superDispatchTrackballEvent(MotionEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean superDispatchGenericMotionEvent(MotionEvent event) {
+ return false;
+ }
+
+ @NonNull
+ @Override
+ public View getDecorView() {
+ return mDecorView;
+ }
+
+ @Override
+ public View peekDecorView() {
+ return null;
+ }
+
+ @Override
+ public Bundle saveHierarchyState() {
+ return null;
+ }
+
+ @Override
+ public void restoreHierarchyState(Bundle savedInstanceState) {
+
+ }
+
+ @Override
+ protected void onActive() {
+
+ }
+
+ @Override
+ public void setChildDrawable(int featureId, Drawable drawable) {
+
+ }
+
+ @Override
+ public void setChildInt(int featureId, int value) {
+
+ }
+
+ @Override
+ public boolean isShortcutKey(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public void setVolumeControlStream(int streamType) {
+
+ }
+
+ @Override
+ public int getVolumeControlStream() {
+ return 0;
+ }
+
+ @Override
+ public int getStatusBarColor() {
+ return 0;
+ }
+
+ @Override
+ public void setStatusBarColor(int color) {
+
+ }
+
+ @Override
+ public int getNavigationBarColor() {
+ return 0;
+ }
+
+ @Override
+ public void setNavigationBarColor(int color) {
+
+ }
+
+ @Override
+ public void setDecorCaptionShade(int decorCaptionShade) {
+
+ }
+
+ @Override
+ public void setResizingCaptionDrawable(Drawable drawable) {
+
+ }
+}
diff --git a/window/window/src/test/java/androidx/window/TestWindowBoundsHelper.java b/window/window/src/test/java/androidx/window/TestWindowBoundsHelper.java
new file mode 100644
index 0000000..15a069e
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/TestWindowBoundsHelper.java
@@ -0,0 +1,100 @@
+/*
+ * 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;
+
+import android.app.Activity;
+import android.graphics.Rect;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.HashMap;
+
+/**
+ * Subclass of {@link WindowBoundsHelper} used to override the results for testing.
+ *
+ * @see WindowBoundsHelper
+ * @see WindowBoundsHelper#setForTesting(WindowBoundsHelper)
+ */
+class TestWindowBoundsHelper extends WindowBoundsHelper {
+ private Rect mGlobalOverriddenBounds;
+ private final HashMap<Activity, Rect> mOverriddenBounds = new HashMap<>();
+ private final HashMap<Activity, Rect> mOverriddenMaximumBounds = new HashMap<>();
+
+ /**
+ * Overrides the bounds returned from this helper for the given context. Passing null {@code
+ * bounds} has the effect of clearing the bounds override.
+ * <p>
+ * Note: A global override set as a result of {@link #setCurrentBounds(Rect)} takes precedence
+ * over the value set with this method.
+ */
+ void setCurrentBoundsForActivity(@NonNull Activity activity, @Nullable Rect bounds) {
+ mOverriddenBounds.put(activity, bounds);
+ }
+
+ /**
+ * Overrides the max bounds returned from this helper for the given context. Passing {@code
+ * null} {@code bounds} has the effect of clearing the bounds override.
+ */
+ void setMaximumBoundsForActivity(@NonNull Activity activity, @Nullable Rect bounds) {
+ mOverriddenMaximumBounds.put(activity, bounds);
+ }
+
+ /**
+ * Overrides the bounds returned from this helper for all supplied contexts. Passing null
+ * {@code bounds} has the effect of clearing the global override.
+ */
+ void setCurrentBounds(@Nullable Rect bounds) {
+ mGlobalOverriddenBounds = bounds;
+ }
+
+ @Override
+ @NonNull
+ Rect computeCurrentWindowBounds(Activity activity) {
+ if (mGlobalOverriddenBounds != null) {
+ return mGlobalOverriddenBounds;
+ }
+
+ Rect bounds = mOverriddenBounds.get(activity);
+ if (bounds != null) {
+ return bounds;
+ }
+
+ return super.computeCurrentWindowBounds(activity);
+ }
+
+ @NonNull
+ @Override
+ Rect computeMaximumWindowBounds(Activity activity) {
+ Rect bounds = mOverriddenMaximumBounds.get(activity);
+ if (bounds != null) {
+ return bounds;
+ }
+
+ return super.computeMaximumWindowBounds(activity);
+ }
+
+ /**
+ * Clears any overrides set with {@link #setCurrentBounds(Rect)} or
+ * {@link #setCurrentBoundsForActivity(Activity, Rect)}.
+ */
+ void reset() {
+ mGlobalOverriddenBounds = null;
+ mOverriddenBounds.clear();
+ mOverriddenMaximumBounds.clear();
+ }
+}