Merge "Bump Room to 2.3.0-rc01" into androidx-main
diff --git a/activity/activity-compose/integration-tests/activity-demos/lint-baseline.xml b/activity/activity-compose/integration-tests/activity-demos/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/activity/activity-compose/integration-tests/activity-demos/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/activity/activity-compose/lint-baseline.xml b/activity/activity-compose/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/activity/activity-compose/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/activity/activity-compose/samples/lint-baseline.xml b/activity/activity-compose/samples/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/activity/activity-compose/samples/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/activity/activity-ktx/lint-baseline.xml b/activity/activity-ktx/lint-baseline.xml
deleted file mode 100644
index 8f1aa4b..0000000
--- a/activity/activity-ktx/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/activity/activity-lint/lint-baseline.xml b/activity/activity-lint/lint-baseline.xml
deleted file mode 100644
index 297ae16..0000000
--- a/activity/activity-lint/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" version="4.2.0-beta02">
-
-</issues>
diff --git a/activity/activity/lint-baseline.xml b/activity/activity/lint-baseline.xml
index 3c4f648..7819beb 100644
--- a/activity/activity/lint-baseline.xml
+++ b/activity/activity/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
+<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
<issue
id="BanTargetApiAnnotation"
@@ -8,7 +8,7 @@
errorLine2=" ~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/activity/result/contract/ActivityResultContracts.java"
- line="458"
+ line="459"
column="5"/>
</issue>
@@ -19,7 +19,7 @@
errorLine2=" ~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/activity/result/contract/ActivityResultContracts.java"
- line="521"
+ line="523"
column="5"/>
</issue>
@@ -30,7 +30,7 @@
errorLine2=" ~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/activity/result/contract/ActivityResultContracts.java"
- line="559"
+ line="561"
column="5"/>
</issue>
@@ -41,7 +41,7 @@
errorLine2=" ~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/activity/result/contract/ActivityResultContracts.java"
- line="601"
+ line="603"
column="5"/>
</issue>
@@ -52,7 +52,7 @@
errorLine2=" ~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/activity/result/contract/ActivityResultContracts.java"
- line="640"
+ line="642"
column="5"/>
</issue>
@@ -63,7 +63,7 @@
errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/java/androidx/activity/result/contract/ActivityResultContracts.java"
- line="495"
+ line="497"
column="40"/>
</issue>
@@ -74,7 +74,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/activity/ComponentActivity.java"
- line="246"
+ line="232"
column="35"/>
</issue>
diff --git a/annotation/annotation-experimental-lint/integration-tests/lint-baseline.xml b/annotation/annotation-experimental-lint/integration-tests/lint-baseline.xml
index 29c92ce..7f7d50f 100644
--- a/annotation/annotation-experimental-lint/integration-tests/lint-baseline.xml
+++ b/annotation/annotation-experimental-lint/integration-tests/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
+<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
<issue
id="UnsafeOptInUsageError"
diff --git a/annotation/annotation-experimental-lint/lint-baseline.xml b/annotation/annotation-experimental-lint/lint-baseline.xml
deleted file mode 100644
index 297ae16..0000000
--- a/annotation/annotation-experimental-lint/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" version="4.2.0-beta02">
-
-</issues>
diff --git a/annotation/annotation-experimental/lint-baseline.xml b/annotation/annotation-experimental/lint-baseline.xml
deleted file mode 100644
index 8f1aa4b..0000000
--- a/annotation/annotation-experimental/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/annotation/annotation-experimental/proguard-rules.pro b/annotation/annotation-experimental/proguard-rules.pro
index 8392750..d1c676bf 100644
--- a/annotation/annotation-experimental/proguard-rules.pro
+++ b/annotation/annotation-experimental/proguard-rules.pro
@@ -14,7 +14,9 @@
# Ignore missing Kotlin meta-annotations so that this library can be used
# without adding a compileOnly dependency on the Kotlin standard library.
+-dontwarn kotlin.Deprecated
-dontwarn kotlin.Metadata
+-dontwarn kotlin.ReplaceWith
-dontwarn kotlin.annotation.AnnotationRetention
-dontwarn kotlin.annotation.AnnotationTarget
-dontwarn kotlin.annotation.Retention
diff --git a/annotation/annotation-sampled/lint-baseline.xml b/annotation/annotation-sampled/lint-baseline.xml
deleted file mode 100644
index 297ae16..0000000
--- a/annotation/annotation-sampled/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" version="4.2.0-beta02">
-
-</issues>
diff --git a/annotation/annotation/lint-baseline.xml b/annotation/annotation/lint-baseline.xml
deleted file mode 100644
index 297ae16..0000000
--- a/annotation/annotation/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" version="4.2.0-beta02">
-
-</issues>
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LayoutInflaterFactoryTestCase.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LayoutInflaterFactoryTestCase.java
index 1b0a1da..15d4194 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LayoutInflaterFactoryTestCase.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LayoutInflaterFactoryTestCase.java
@@ -92,6 +92,16 @@
@UiThreadTest
@Test
@SmallTest
+ public void testAndroidThemeWithIncludeInflation() {
+ LayoutInflater inflater = LayoutInflater.from(mActivityTestRule.getActivity());
+ final ViewGroup root = (ViewGroup) inflater.inflate(
+ R.layout.layout_android_theme_with_include, null);
+ assertThemedContext(root.findViewById(R.id.included_view));
+ }
+
+ @UiThreadTest
+ @Test
+ @SmallTest
public void testThemedInflationWithUnattachedParent() {
final Context activity = mActivityTestRule.getActivity();
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeRotateRecreatesActivityWithConfigTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeRotateRecreatesActivityWithConfigTestCase.kt
index 9a73838..f3e217c 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeRotateRecreatesActivityWithConfigTestCase.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/NightModeRotateRecreatesActivityWithConfigTestCase.kt
@@ -28,6 +28,7 @@
import androidx.lifecycle.Lifecycle
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.filters.FlakyTest
import androidx.test.filters.LargeTest
import androidx.testutils.LifecycleOwnerUtils
import org.junit.After
@@ -59,6 +60,7 @@
}
}
+ @FlakyTest // b/182209264
@Test
public fun testRotateRecreatesActivityWithConfig() {
// Don't run this test on SDK 26 because it has issues with setRequestedOrientation. Also
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextReceiveContentTest.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextReceiveContentTest.java
index 3fc3605..b532d36 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextReceiveContentTest.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextReceiveContentTest.java
@@ -40,6 +40,7 @@
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
+import android.os.Parcelable;
import android.text.SpannableStringBuilder;
import android.view.DragEvent;
import android.view.inputmethod.EditorInfo;
@@ -525,7 +526,9 @@
}
EditorInfo editorInfo = new EditorInfo();
InputConnection ic = mEditText.onCreateInputConnection(editorInfo);
- return InputConnectionCompat.commitContent(ic, editorInfo, contentInfo, 0, opts);
+ int flags = (Build.VERSION.SDK_INT >= 25)
+ ? InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION : 0;
+ return InputConnectionCompat.commitContent(ic, editorInfo, contentInfo, flags, opts);
}
private boolean triggerImeCommitContentDirect(String mimeType) {
@@ -535,7 +538,9 @@
null);
EditorInfo editorInfo = new EditorInfo();
InputConnection ic = mEditText.onCreateInputConnection(editorInfo);
- return ic.commitContent(contentInfo, 0, null);
+ int flags = (Build.VERSION.SDK_INT >= 25)
+ ? InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION : 0;
+ return ic.commitContent(contentInfo, flags, null);
}
private boolean triggerDropEvent(ClipData clip) {
@@ -594,15 +599,15 @@
@Nullable
private final Uri mLinkUri;
@Nullable
- private final String mExtra;
+ private final String mExtraValue;
private PayloadArgumentMatcher(@NonNull ClipData clip, int source, int flags,
- @Nullable Uri linkUri, @Nullable String extra) {
+ @Nullable Uri linkUri, @Nullable String extraValue) {
mClip = clip;
mSource = source;
mFlags = flags;
mLinkUri = linkUri;
- mExtra = extra;
+ mExtraValue = extraValue;
}
@Override
@@ -618,11 +623,16 @@
}
private boolean extrasMatch(Bundle actualExtras) {
- if (mExtra == null) {
+ if (mSource == SOURCE_INPUT_METHOD && Build.VERSION.SDK_INT >= 25) {
+ assertThat(actualExtras).isNotNull();
+ Parcelable actualInputContentInfoExtra = actualExtras.getParcelable(
+ "androidx.core.view.extra.INPUT_CONTENT_INFO");
+ assertThat(actualInputContentInfoExtra).isInstanceOf(InputContentInfo.class);
+ } else if (mExtraValue == null) {
return actualExtras == null;
}
String actualExtraValue = actualExtras.getString(EXTRA_KEY);
- return ObjectsCompat.equals(mExtra, actualExtraValue);
+ return ObjectsCompat.equals(mExtraValue, actualExtraValue);
}
}
}
diff --git a/appcompat/appcompat/src/androidTest/res/layout/layout_android_theme_included_view.xml b/appcompat/appcompat/src/androidTest/res/layout/layout_android_theme_included_view.xml
new file mode 100644
index 0000000..03c8d42
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/res/layout/layout_android_theme_included_view.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Test" />
diff --git a/appcompat/appcompat/src/androidTest/res/layout/layout_android_theme_with_include.xml b/appcompat/appcompat/src/androidTest/res/layout/layout_android_theme_with_include.xml
new file mode 100644
index 0000000..f5e6dc9
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/res/layout/layout_android_theme_with_include.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:theme="@style/MagentaThemeOverlay">
+
+ <include layout="@layout/layout_android_theme_included_view"
+ android:id="@+id/included_view" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java
index 22792d7..90f4b18 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java
@@ -281,6 +281,7 @@
private Rect mTempRect2;
private AppCompatViewInflater mAppCompatViewInflater;
+ private LayoutIncludeDetector mLayoutIncludeDetector;
AppCompatDelegateImpl(Activity activity, AppCompatCallback callback) {
this(activity, null, callback, activity);
@@ -1543,11 +1544,20 @@
boolean inheritContext = false;
if (IS_PRE_LOLLIPOP) {
- inheritContext = (attrs instanceof XmlPullParser)
- // If we have a XmlPullParser, we can detect where we are in the layout
- ? ((XmlPullParser) attrs).getDepth() > 1
- // Otherwise we have to use the old heuristic
- : shouldInheritContext((ViewParent) parent);
+ if (mLayoutIncludeDetector == null) {
+ mLayoutIncludeDetector = new LayoutIncludeDetector();
+ }
+ if (mLayoutIncludeDetector.detect(attrs)) {
+ // The view being inflated is the root of an <include>d view, so make sure
+ // we carry over any themed context.
+ inheritContext = true;
+ } else {
+ inheritContext = (attrs instanceof XmlPullParser)
+ // If we have a XmlPullParser, we can detect where we are in the layout
+ ? ((XmlPullParser) attrs).getDepth() > 1
+ // Otherwise we have to use the old heuristic
+ : shouldInheritContext((ViewParent) parent);
+ }
}
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/app/LayoutIncludeDetector.java b/appcompat/appcompat/src/main/java/androidx/appcompat/app/LayoutIncludeDetector.java
new file mode 100644
index 0000000..71963ec
--- /dev/null
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/app/LayoutIncludeDetector.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appcompat.app;
+
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayDeque;
+import java.util.Deque;
+
+/**
+ * Used on KitKat and below to determine if the currently inflated view is the start of
+ * an included layout file. If so, any themed context (android:theme) needs to be manually
+ * carried over to preserve it as expected.
+ */
+class LayoutIncludeDetector {
+
+ @NonNull
+ private final Deque<WeakReference<XmlPullParser>> mXmlParserStack = new ArrayDeque<>();
+
+ /**
+ * Returns true if this is the start of an included layout file, otherwise false.
+ */
+ boolean detect(@NonNull AttributeSet attrs) {
+ if (attrs instanceof XmlPullParser) {
+ XmlPullParser xmlAttrs = (XmlPullParser) attrs;
+ if (xmlAttrs.getDepth() == 1) {
+ // This is either beginning of an inflate or an include.
+ // Start by popping XmlPullParsers which are no longer valid since we may
+ // have returned from any number of sub-includes
+ XmlPullParser ancestorXmlAttrs = popOutdatedAttrHolders(mXmlParserStack);
+ // Then store current attrs for possible future use
+ mXmlParserStack.push(new WeakReference<>(xmlAttrs));
+ // Finally check if we need to inherit the parent context based on the
+ // current and ancestor attribute set
+ if (shouldInheritContext(xmlAttrs, ancestorXmlAttrs)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static boolean shouldInheritContext(@NonNull XmlPullParser parser,
+ @Nullable XmlPullParser previousParser) {
+ if (previousParser != null && parser != previousParser) {
+
+ // First check event type since that should avoid accessing native side with
+ // possibly nulled native ptr. We do this since the previous parser could be
+ // either the parent parser for an <include> (in which case it is still active,
+ // with event type == START_TAG) or a parser from a separate inflate() call (in
+ // which case the parser has been closed and would typically have event type
+ // END_TAG or possibly END_DOCUMENT)
+ try {
+ if (previousParser.getEventType() == XmlPullParser.START_TAG) {
+ // Check if the parent parser is actually on an <include> tag,
+ // if so we need to inherit the parent context
+ return "include".equals(previousParser.getName());
+ }
+ } catch (XmlPullParserException e) {
+ }
+ }
+ return false;
+ }
+
+
+ /**
+ * Pops any outdated {@link XmlPullParser}s from the given stack.
+ * @param xmlParserStack stack to purge
+ * @return most recent {@link XmlPullParser} that is not outdated
+ */
+ @Nullable
+ private static XmlPullParser popOutdatedAttrHolders(@NonNull
+ Deque<WeakReference<XmlPullParser>> xmlParserStack) {
+ while (!xmlParserStack.isEmpty()) {
+ XmlPullParser parser = xmlParserStack.peek().get();
+ if (isParserOutdated(parser)) {
+ xmlParserStack.pop();
+ } else {
+ return parser;
+ }
+ }
+ return null;
+ }
+
+ private static boolean isParserOutdated(@Nullable XmlPullParser parser) {
+ try {
+ return parser == null || (parser.getEventType() == XmlPullParser.END_TAG
+ || parser.getEventType() == XmlPullParser.END_DOCUMENT);
+ } catch (XmlPullParserException e) {
+ return true;
+ }
+ }
+}
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatReceiveContentHelper.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatReceiveContentHelper.java
index 7f90cef..af01fd7 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatReceiveContentHelper.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatReceiveContentHelper.java
@@ -20,6 +20,7 @@
import static androidx.core.view.ContentInfoCompat.SOURCE_CLIPBOARD;
import static androidx.core.view.ContentInfoCompat.SOURCE_DRAG_AND_DROP;
import static androidx.core.view.ContentInfoCompat.SOURCE_INPUT_METHOD;
+import static androidx.core.view.inputmethod.InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION;
import android.app.Activity;
import android.content.ClipData;
@@ -34,6 +35,7 @@
import android.view.DragEvent;
import android.view.View;
import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputContentInfo;
import android.widget.TextView;
import androidx.annotation.NonNull;
@@ -178,7 +180,9 @@
@Override
public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags,
Bundle opts) {
- if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
+ Bundle extras = opts;
+ if (Build.VERSION.SDK_INT >= 25
+ && (flags & INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
try {
inputContentInfo.requestPermission();
} catch (Exception e) {
@@ -186,15 +190,33 @@
"Can't insert content from IME; requestPermission() failed", e);
return false;
}
+ // Permissions granted above are revoked automatically by the platform when the
+ // corresponding InputContentInfo object is garbage collected. To prevent
+ // this from happening prematurely (before the receiving app has had a chance
+ // to process the content), we set the InputContentInfo object into the
+ // extras of the payload passed to OnReceiveContentListener.
+ InputContentInfo inputContentInfoFmk =
+ (InputContentInfo) inputContentInfo.unwrap();
+ extras = (opts == null) ? new Bundle() : new Bundle(opts);
+ extras.putParcelable(EXTRA_INPUT_CONTENT_INFO, inputContentInfoFmk);
}
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)
+ .setExtras(extras)
.build();
return ViewCompat.performReceiveContent(view, payload) == null;
}
};
}
+
+ /**
+ * Key for extras in {@link ContentInfoCompat}, to hold the {@link InputContentInfo} object
+ * passed by the IME. Apps should not access/read this object; it is only set in the extras
+ * in order to prevent premature garbage collection of {@link InputContentInfo} which in
+ * turn causes premature revocation of URI permissions.
+ */
+ private static final String EXTRA_INPUT_CONTENT_INFO =
+ "androidx.core.view.extra.INPUT_CONTENT_INFO";
}
diff --git a/benchmark/benchmark/lint-baseline.xml b/benchmark/benchmark/lint-baseline.xml
deleted file mode 100644
index 8f1aa4b..0000000
--- a/benchmark/benchmark/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/benchmark/common/api/public_plus_experimental_current.txt b/benchmark/common/api/public_plus_experimental_current.txt
index aa84523..6f98a41 100644
--- a/benchmark/common/api/public_plus_experimental_current.txt
+++ b/benchmark/common/api/public_plus_experimental_current.txt
@@ -1,15 +1,6 @@
// Signature format: 4.0
package androidx.benchmark {
- @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final class Arguments {
- method public String getOutputDirectoryPath();
- method public java.util.Set<java.lang.String> getSuppressedErrors();
- method public java.io.File testOutputFile(String filename);
- property public final String outputDirectoryPath;
- property public final java.util.Set<java.lang.String> suppressedErrors;
- field public static final androidx.benchmark.Arguments INSTANCE;
- }
-
public final class ArgumentsKt {
}
diff --git a/benchmark/common/lint-baseline.xml b/benchmark/common/lint-baseline.xml
index d929aa6..a6e93ad 100644
--- a/benchmark/common/lint-baseline.xml
+++ b/benchmark/common/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
+<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
<issue
id="BanSynchronizedMethods"
@@ -111,15 +111,4 @@
column="30"/>
</issue>
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 21, the call containing class androidx.benchmark.ProfilerKt is not annotated with @RequiresApi(x) where x is at least 21. Either annotate the containing class with at least @RequiresApi(21) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(21)."
- errorLine1=" Debug.startMethodTracingSampling(path, bufferSize, Arguments.profilerSampleFrequency)"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/benchmark/Profiler.kt"
- line="99"
- column="15"/>
- </issue>
-
</issues>
diff --git a/benchmark/common/src/androidTest/java/androidx/benchmark/BenchmarkStateTest.kt b/benchmark/common/src/androidTest/java/androidx/benchmark/BenchmarkStateTest.kt
index ef9b83d..2c6518d 100644
--- a/benchmark/common/src/androidTest/java/androidx/benchmark/BenchmarkStateTest.kt
+++ b/benchmark/common/src/androidTest/java/androidx/benchmark/BenchmarkStateTest.kt
@@ -36,11 +36,11 @@
@LargeTest
@RunWith(AndroidJUnit4::class)
-class BenchmarkStateTest {
+public class BenchmarkStateTest {
private fun us2ns(ms: Long): Long = TimeUnit.MICROSECONDS.toNanos(ms)
@get:Rule
- val writePermissionRule =
+ public val writePermissionRule: GrantPermissionRule =
GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)!!
/**
@@ -58,7 +58,7 @@
}
@Test
- fun validateMetrics() {
+ public fun validateMetrics() {
val state = BenchmarkState()
while (state.keepRunning()) {
runAndSpin(durationUs = 300) {
@@ -88,7 +88,7 @@
}
@Test
- fun keepRunningMissingResume() {
+ public fun keepRunningMissingResume() {
val state = BenchmarkState()
assertEquals(true, state.keepRunning())
@@ -97,7 +97,7 @@
}
@Test
- fun pauseCalledTwice() {
+ public fun pauseCalledTwice() {
val state = BenchmarkState()
assertEquals(true, state.keepRunning())
@@ -107,7 +107,7 @@
@SdkSuppress(minSdkVersion = 24)
@Test
- fun priorityJitThread() {
+ public fun priorityJitThread() {
assertEquals(
"JIT priority should not yet be modified",
ThreadPriority.JIT_INITIAL_PRIORITY,
@@ -128,7 +128,7 @@
}
@Test
- fun priorityBenchThread() {
+ public fun priorityBenchThread() {
val initialPriority = ThreadPriority.get()
assertNotEquals(
"Priority should not be max",
@@ -172,12 +172,12 @@
}
@Test
- fun iterationCheck_simple() {
+ public fun iterationCheck_simple() {
iterationCheck(checkingForThermalThrottling = true)
}
@Test
- fun iterationCheck_withAllocations() {
+ public fun iterationCheck_withAllocations() {
if (CpuInfo.locked ||
IsolationActivity.sustainedPerformanceModeInUse ||
Errors.isEmulator
@@ -191,7 +191,7 @@
}
@Test
- fun bundle() {
+ public fun bundle() {
val bundle = BenchmarkState().apply {
while (keepRunning()) {
// nothing, we're ignoring numbers
@@ -221,7 +221,7 @@
}
@Test
- fun notStarted() {
+ public fun notStarted() {
val initialPriority = ThreadPriority.get()
try {
BenchmarkState().getReport().getStats("timeNs").median
@@ -234,7 +234,7 @@
}
@Test
- fun notFinished() {
+ public fun notFinished() {
val initialPriority = ThreadPriority.get()
try {
BenchmarkState().run {
@@ -252,7 +252,7 @@
@Suppress("DEPRECATION")
@UseExperimental(ExperimentalExternalReport::class)
@Test
- fun reportResult() {
+ public fun reportResult() {
BenchmarkState.reportData(
className = "className",
testName = "testName",
diff --git a/benchmark/common/src/androidTest/java/androidx/benchmark/InstrumentationResultsTest.kt b/benchmark/common/src/androidTest/java/androidx/benchmark/InstrumentationResultsTest.kt
index bfd01c8..fbd23ee 100644
--- a/benchmark/common/src/androidTest/java/androidx/benchmark/InstrumentationResultsTest.kt
+++ b/benchmark/common/src/androidTest/java/androidx/benchmark/InstrumentationResultsTest.kt
@@ -24,7 +24,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
-class InstrumentationResultsTest {
+public class InstrumentationResultsTest {
@Test
public fun ideSummary_alignment() {
val summary1 = InstrumentationResults.ideSummaryLine("foo", 1000, 100)
diff --git a/benchmark/common/src/androidTest/java/androidx/benchmark/MetricCaptureTest.kt b/benchmark/common/src/androidTest/java/androidx/benchmark/MetricCaptureTest.kt
index 5f652a0..285fc54 100644
--- a/benchmark/common/src/androidTest/java/androidx/benchmark/MetricCaptureTest.kt
+++ b/benchmark/common/src/androidTest/java/androidx/benchmark/MetricCaptureTest.kt
@@ -23,16 +23,16 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
-class AllocationCountCaptureTest {
+public class AllocationCountCaptureTest {
@Test
- fun simple() {
+ public fun simple() {
AllocationCountCapture().verifyMedian(100..110) {
allocate(100)
}
}
@Test
- fun pauseResume() {
+ public fun pauseResume() {
AllocationCountCapture().verifyMedian(100..110) {
allocate(100)
diff --git a/benchmark/common/src/androidTest/java/androidx/benchmark/ProfilerTest.kt b/benchmark/common/src/androidTest/java/androidx/benchmark/ProfilerTest.kt
index acc43f2..6e2f2be 100644
--- a/benchmark/common/src/androidTest/java/androidx/benchmark/ProfilerTest.kt
+++ b/benchmark/common/src/androidTest/java/androidx/benchmark/ProfilerTest.kt
@@ -31,9 +31,9 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
-class ProfilerTest {
+public class ProfilerTest {
@Test
- fun getByName() {
+ public fun getByName() {
assertSame(MethodSampling, Profiler.getByName("MethodSampling"))
assertSame(MethodTracing, Profiler.getByName("MethodTracing"))
assertSame(ConnectedAllocation, Profiler.getByName("ConnectedAllocation"))
@@ -69,15 +69,15 @@
}
@Test
- fun methodSampling() = verifyProfiler(
+ public fun methodSampling(): Unit = verifyProfiler(
profiler = MethodSampling,
- file = Arguments.testOutputFile("test-methodSampling.trace")
+ file = Outputs.testOutputFile("test-methodSampling.trace")
)
@Test
- fun methodTracing() = verifyProfiler(
+ public fun methodTracing(): Unit = verifyProfiler(
profiler = MethodTracing,
- file = Arguments.testOutputFile("test-methodTracing.trace")
+ file = Outputs.testOutputFile("test-methodTracing.trace")
)
@Ignore(
@@ -86,7 +86,7 @@
)
@SdkSuppress(minSdkVersion = 28)
@Test
- fun methodSamplingSimpleperf() = verifyProfiler(
+ public fun methodSamplingSimpleperf(): Unit = verifyProfiler(
profiler = MethodSamplingSimpleperf,
file = File("/data/data/androidx.benchmark.test/simpleperf_data/test.data")
)
diff --git a/benchmark/common/src/androidTest/java/androidx/benchmark/ResultWriterTest.kt b/benchmark/common/src/androidTest/java/androidx/benchmark/ResultWriterTest.kt
index 8712916..3b91c72 100644
--- a/benchmark/common/src/androidTest/java/androidx/benchmark/ResultWriterTest.kt
+++ b/benchmark/common/src/androidTest/java/androidx/benchmark/ResultWriterTest.kt
@@ -30,7 +30,7 @@
@RunWith(AndroidJUnit4::class)
public class ResultWriterTest {
@get:Rule
- val tempFolder = TemporaryFolder()
+ public val tempFolder: TemporaryFolder = TemporaryFolder()
private val metricResults = listOf(
MetricResult(
@@ -59,7 +59,7 @@
)
@Test
- fun shouldClearExistingContent() {
+ public fun shouldClearExistingContent() {
val tempFile = tempFolder.newFile()
val fakeText = "This text should not be in the final output"
@@ -70,7 +70,7 @@
}
@Test
- fun validateJson() {
+ public fun validateJson() {
val tempFile = tempFolder.newFile()
val sustainedPerformanceModeInUse = IsolationActivity.sustainedPerformanceModeInUse
@@ -146,7 +146,7 @@
}
@Test
- fun validateJsonWithParams() {
+ public fun validateJsonWithParams() {
val reportWithParams = BenchmarkResult(
testName = "MethodWithParams[number=2,primeNumber=true]",
className = "package.Class",
@@ -175,7 +175,7 @@
}
@Test
- fun validateJsonWithInvalidParams() {
+ public fun validateJsonWithInvalidParams() {
val reportWithInvalidParams = BenchmarkResult(
testName = "MethodWithParams[number=2,=true,]",
className = "package.Class",
diff --git a/benchmark/common/src/androidTest/java/androidx/benchmark/WarmupManagerTest.kt b/benchmark/common/src/androidTest/java/androidx/benchmark/WarmupManagerTest.kt
index c1d0efd..9b2eac8 100644
--- a/benchmark/common/src/androidTest/java/androidx/benchmark/WarmupManagerTest.kt
+++ b/benchmark/common/src/androidTest/java/androidx/benchmark/WarmupManagerTest.kt
@@ -26,10 +26,10 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
-class WarmupManagerTest {
+public class WarmupManagerTest {
@Test
- fun minDuration() {
+ public fun minDuration() {
// even with tiny, predictable inputs, we require min warmup duration
val warmup = WarmupManager()
while (!warmup.onNextIteration(100)) {}
@@ -37,7 +37,7 @@
}
@Test
- fun maxDuration() {
+ public fun maxDuration() {
// even if values are warming up slowly, we require max warmup duration
val warmup = WarmupManager()
warmup.warmupOnFakeData(
@@ -53,7 +53,7 @@
}
@Test
- fun minIterations() {
+ public fun minIterations() {
// min iterations overrides max duration
val warmup = WarmupManager()
while (!warmup.onNextIteration(TimeUnit.SECONDS.toNanos(2))) {}
@@ -61,7 +61,7 @@
}
@Test
- fun similarIterationCount() {
+ public fun similarIterationCount() {
// mock warmup data, and validate we detect convergence
val warmup = WarmupManager()
val warmupNeededNs = TimeUnit.SECONDS.toNanos(2)
diff --git a/benchmark/common/src/main/java/androidx/benchmark/Arguments.kt b/benchmark/common/src/main/java/androidx/benchmark/Arguments.kt
index 857759e..c14a978 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/Arguments.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/Arguments.kt
@@ -20,21 +20,23 @@
import android.util.Log
import androidx.annotation.RestrictTo
import androidx.test.platform.app.InstrumentationRegistry
-import java.io.File
/**
* This allows tests to override arguments from code
*
- * @suppress
+ * @hide
*/
@RestrictTo(RestrictTo.Scope.TESTS)
public var argumentSource: Bundle? = null
+/**
+ * @hide
+ */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public object Arguments {
+
// public properties are shared by micro + macro benchmarks
public val suppressedErrors: Set<String>
- public val outputDirectoryPath: String
// internal properties are microbenchmark only
internal val outputEnable: Boolean
@@ -45,8 +47,7 @@
internal val profilerSampleDurationSeconds: Long
internal var error: String? = null
-
- private val testOutputDir: File
+ internal val additionalTestOutputDir: String?
private const val prefix = "androidx.benchmark."
@@ -105,19 +106,6 @@
"$profilerSampleFrequency, duration $profilerSampleDurationSeconds"
)
}
-
- val additionalTestOutputDir = arguments.getString("additionalTestOutputDir")
- testOutputDir = additionalTestOutputDir?.let { File(it) }
- ?: InstrumentationRegistry.getInstrumentation().context.externalCacheDir
- ?: throw IllegalStateException(
- "Unable to read externalCacheDir for writing files, " +
- "additionalTestOutputDir argument required to declare output dir."
- )
-
- outputDirectoryPath = testOutputDir.path
+ additionalTestOutputDir = arguments.getString("additionalTestOutputDir")
}
-
- public fun testOutputFile(filename: String): File {
- return File(testOutputDir, filename)
- }
-}
\ No newline at end of file
+}
diff --git a/benchmark/common/src/main/java/androidx/benchmark/InstrumentationResults.kt b/benchmark/common/src/main/java/androidx/benchmark/InstrumentationResults.kt
index 7ac7359..ad96275 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/InstrumentationResults.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/InstrumentationResults.kt
@@ -39,7 +39,9 @@
summaryV2: String = summaryV1
) {
bundle.putString(IDE_V1_SUMMARY_KEY, summaryV1)
- bundle.putString(IDE_V2_OUTPUT_DIR_PATH_KEY, Arguments.outputDirectoryPath)
+ // Outputs.outputDirectory is safe to use in the context of Studio currently.
+ // This is because AGP does not populate the `additionalTestOutputDir` argument.
+ bundle.putString(IDE_V2_OUTPUT_DIR_PATH_KEY, Outputs.outputDirectory.absolutePath)
bundle.putString(IDE_V2_SUMMARY_KEY, summaryV2)
}
diff --git a/benchmark/common/src/main/java/androidx/benchmark/Outputs.kt b/benchmark/common/src/main/java/androidx/benchmark/Outputs.kt
new file mode 100644
index 0000000..ae2b62c
--- /dev/null
+++ b/benchmark/common/src/main/java/androidx/benchmark/Outputs.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark
+
+import android.annotation.SuppressLint
+import android.os.Build
+import android.os.Environment
+import android.util.Log
+import androidx.annotation.RestrictTo
+import androidx.test.platform.app.InstrumentationRegistry
+import java.io.File
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public object Outputs {
+
+ /**
+ * The output directory that the developer wants us to use.
+ */
+ public val outputDirectory: File
+
+ /**
+ * The usable output directory, given permission issues with `adb shell` on Android R.
+ * Both the app and the shell have access to this output folder.
+ */
+ public val dirUsableByAppAndShell: File
+
+ init {
+ @SuppressLint("UnsafeNewApiCall", "NewApi")
+ @Suppress("DEPRECATION")
+ dirUsableByAppAndShell = when (Build.VERSION.SDK_INT) {
+ Build.VERSION_CODES.R -> {
+ // On Android R, we are using the media directory because that is the directory
+ // that the shell has access to. Context: b/181601156
+ InstrumentationRegistry.getInstrumentation().context.externalMediaDirs
+ .firstOrNull {
+ Environment.getExternalStorageState(it) == Environment.MEDIA_MOUNTED
+ }
+ }
+ else -> InstrumentationRegistry.getInstrumentation().context.externalCacheDir
+ } ?: throw IllegalStateException(
+ "Unable to read externalCacheDir for writing files, " +
+ "additionalTestOutputDir argument required to declare output dir."
+ )
+
+ Log.d(BenchmarkState.TAG, "Usable output directory: $dirUsableByAppAndShell")
+
+ outputDirectory = Arguments.additionalTestOutputDir?.let { File(it) }
+ ?: dirUsableByAppAndShell
+
+ Log.d(BenchmarkState.TAG, "Output Directory: $outputDirectory")
+ }
+
+ /**
+ * Create a benchmark output [File] to write to.
+ *
+ * This method handles reporting files to `InstrumentationStatus` to request copy,
+ * writing them in the desired output directory, and handling shell access issues on Android R.
+ *
+ * @return The absolute path of the output [File].
+ */
+ public fun writeFile(
+ fileName: String,
+ reportKey: String,
+ reportOnRunEndOnly: Boolean = false,
+ block: (file: File) -> Unit,
+ ): String {
+ val override = Build.VERSION.SDK_INT == Build.VERSION_CODES.R
+ // We override the `additionalTestOutputDir` argument on R.
+ // Context: b/181601156
+ val file = File(dirUsableByAppAndShell, fileName)
+ try {
+ block.invoke(file)
+ } finally {
+ var destination = file
+ if (override) {
+ // This respects the `additionalTestOutputDir` argument.
+ val actualOutputDirectory = outputDirectory
+ destination = File(actualOutputDirectory, fileName)
+ if (file != destination) {
+ try {
+ destination.mkdirs()
+ file.copyTo(destination, overwrite = true)
+ } catch (exception: Throwable) {
+ // This can happen when `additionalTestOutputDir` being passed in cannot
+ // be written to. The shell does not have permissions to do the necessary
+ // setup, and this can cause `adb pull` to fail.
+ val message = """
+ Unable to copy files to ${destination.absolutePath}.
+ Please pull the Macrobenchmark results manually by using:
+ adb pull ${file.absolutePath}
+ """.trimIndent()
+ Log.e(BenchmarkState.TAG, message, exception)
+ destination = file
+ }
+ }
+ }
+ InstrumentationResults.reportAdditionalFileToCopy(
+ key = reportKey,
+ absoluteFilePath = destination.absolutePath,
+ reportOnRunEndOnly = reportOnRunEndOnly
+ )
+ return destination.absolutePath
+ }
+ }
+
+ public fun testOutputFile(filename: String): File {
+ return File(outputDirectory, filename)
+ }
+
+ public fun relativePathFor(path: String): String {
+ var basePath = outputDirectory.absolutePath
+ val relativePath = if (path.indexOf(basePath) > 0) {
+ path.removePrefix("$basePath/")
+ } else {
+ basePath = dirUsableByAppAndShell.absolutePath
+ path.removePrefix("$basePath/")
+ }
+ check(relativePath != path) {
+ "$relativePath == $path"
+ }
+ return relativePath
+ }
+}
diff --git a/benchmark/common/src/main/java/androidx/benchmark/Profiler.kt b/benchmark/common/src/main/java/androidx/benchmark/Profiler.kt
index 3075cc8..36207f9 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/Profiler.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/Profiler.kt
@@ -16,6 +16,7 @@
package androidx.benchmark
+import android.annotation.SuppressLint
import android.os.Build
import android.os.Debug
import android.util.Log
@@ -85,8 +86,9 @@
}
}
+@SuppressLint("UnsafeNewApiCall")
internal fun startRuntimeMethodTracing(traceFileName: String, sampled: Boolean) {
- val path = Arguments.testOutputFile(traceFileName).absolutePath
+ val path = Outputs.testOutputFile(traceFileName).absolutePath
Log.d(BenchmarkState.TAG, "Profiling output file: $path")
InstrumentationResults.reportAdditionalFileToCopy("profiling_trace", path)
diff --git a/benchmark/common/src/main/java/androidx/benchmark/ResultWriter.kt b/benchmark/common/src/main/java/androidx/benchmark/ResultWriter.kt
index d4d9a78..c539573 100644
--- a/benchmark/common/src/main/java/androidx/benchmark/ResultWriter.kt
+++ b/benchmark/common/src/main/java/androidx/benchmark/ResultWriter.kt
@@ -37,22 +37,21 @@
if (Arguments.outputEnable) {
// Currently, we just overwrite the whole file
// Ideally, append for efficiency
- val packageName =
- InstrumentationRegistry.getInstrumentation().targetContext!!.packageName
+ val packageName = InstrumentationRegistry.getInstrumentation()
+ .targetContext!!
+ .packageName
- val file = Arguments.testOutputFile("$packageName-benchmarkData.json")
- Log.d(
- BenchmarkState.TAG,
- "writing results to ${file.absolutePath}"
- )
- writeReport(file, reports)
- InstrumentationResults.reportAdditionalFileToCopy(
- "results_json",
- file.absolutePath,
- // since we keep appending the same file, defer reporting path until end of suite
- // note: this requires using InstrumentationResultsRunListener
+ Outputs.writeFile(
+ fileName = "$packageName-benchmarkData.json",
+ reportKey = "results_json",
reportOnRunEndOnly = true
- )
+ ) {
+ Log.d(
+ BenchmarkState.TAG,
+ "writing results to ${it.absolutePath}"
+ )
+ writeReport(it, reports)
+ }
} else {
Log.d(
BenchmarkState.TAG,
diff --git a/benchmark/docs/app_audit.md b/benchmark/docs/app_audit.md
new file mode 100644
index 0000000..58916b8
--- /dev/null
+++ b/benchmark/docs/app_audit.md
@@ -0,0 +1,423 @@
+# Android App Audit Runbook
+
+This runbook is intended to give developers the skills to identify and fix key
+performance issues in your app independently.
+
+[TOC]
+
+## Key Performance Issues {#key-performance-issues}
+
+* [Scroll Jank](https://developer.android.com/topic/performance/vitals/render?hl=en#fixing_jank)
+ * "Jank" is the term used to describe the visual hiccup that occurs when
+ the system is not able to build and provide frames in time for them to
+ be drawn to the screen at the requested cadence (60hz, or higher). Jank
+ is most apparent when scrolling, when what should be a smoothly animated
+ flow has "catches".
+ * Apps should target 90Hz refresh rates. Many newer devices such as Pixel
+ 4 operate in 90Hz mode during user interactions like scrolling.
+ * If you're curious what refresh rate your device is using at a given
+ time, you can enable an overlay via Developer Options > Show refresh
+ rate (under Debugging)
+* Transitions that are not smooth
+ * These concerns arise during interactions such as switching between tabs,
+ or loading a new activity. These types of transitions should have smooth
+ animations and not include delays or visual flicker.
+* Power Inefficiency
+ * Doing work costs battery, and doing unnecessary work reduces battery
+ life.
+ * One common cause of unnecessary work can be related to the GC (garbage
+ collector). This can happen when an app is allocating a lot of
+ repetitive variables that it only uses for a short time. Then, the
+ garbage collector needs to run frequently to clean up those variables.
+* [Startup Latency](https://developer.android.com/topic/performance/vitals/launch-time?hl=en#internals)
+ * Startup latency is the amount of time it takes between clicking on the
+ app icon, notification, or other entry point and the user's data being
+ shown on the screen.
+ * We have two startup latency goals:
+ * For "cold" starts, the starts that require the most work from the
+ app and the system, to start consistently within 500ms.
+ * For the P95/P99 latencies to be very close to the median latency.
+ When the app sometimes a very long time to start, user trust is
+ eroded. IPCs and unnecessary I/O during the critical path of app
+ startup can experience lock contention and introduce these
+ inconsistencies.
+
+## Identifying These Issues {#identifying-these-issues}
+
+Generally, the recommended workflow to identify and remedy performance issues is
+as follows:
+
+1. Identify critical user journeys to inspect. These usually include:
+ 1. Common startup flows, including from launcher and notification.
+ 1. Any screens where the user scrolls through data.
+ 1. Transitions between screens.
+ 1. Long-running flows, like navigation or music playback.
+1. Inspect what is happening during those flows using debugging tools:
+ 1. [Systrace/Perfetto](https://developer.android.com/topic/performance/tracing) -
+ Allows you to see exactly what is happening across the entire device
+ with precise timing data.
+ 1. [Memory Profiler](https://developer.android.com/studio/profile/memory-profiler) -
+ Allows you to see what memory allocations are happening on the heap.
+ 1. [Simpleperf](https://developer.android.com/ndk/guides/simpleperf) - View
+ a flamegraph of what function calls are taking up the most CPU during a
+ certain period of time. When you identify something that's taking a long
+ time in systrace, but you don't know why, simpleperf can fill in the
+ blanks.
+
+Manual debugging of individual test runs is **critical** for understanding and
+debugging these performance issues. The above steps **cannot** be replaced by
+analyzing aggregated data. However, setting up metrics collection in automated
+testing as well as in the field is also important to understand what users are
+actually seeing and identify when regressions may occur:
+
+1. Collect metrics during those flows
+ 1. Startup Flows
+ 1. Field metrics
+ 1. [Play Console startup time](https://support.google.com/googleplay/android-developer/answer/9844486#zippy=%2Capp-start-up-time)
+ 2. Lab tests
+ 1. [Jetpack Macrobenchmark: Startup](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/benchmark/docs/macrobenchmark.md#startup)
+ 2. Jank
+ 1. Field metrics
+ 1. Play Console frame vitals
+ 1. Note that within the Play Console, it's not possible to
+ narrow down metrics to a specific user journey, since all
+ that is reported is overall jank throughout the app.
+ 2. Custom measurement with
+ [FrameMetricsAggregator](https://developer.android.com/reference/kotlin/androidx/core/app/FrameMetricsAggregator)
+ 1. You can utilize FrameMetricsAggregator to record Jank
+ metrics during a particular workflow.
+ 2. Lab tests
+ 1. [Jetpack Macrobenchmark: Scrolling](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/benchmark/docs/macrobenchmark.md#scrolling-and-animation)
+ 2. Macrobenchmark collects frame timing via `dumpsys gfxinfo`
+ commands that bracket a user journey in question. This is a
+ reasonable way to understand variation in jank over a specific
+ user journey. The RenderTime metrics, which highlight how long
+ frames are taking to draw, are more important than the count of
+ janky frames for identifying regressions or improvements.
+
+## Setting Up Your App for Performance Analysis {#setting-up-your-app-for-performance-analysis}
+
+Proper setup is essential for getting accurate, repeatable, actionable
+benchmarks from an application. In general, you want to test on a system that is
+as close to production as possible, while suppressing sources of noise. Below
+are a number of APK and system specific steps you can take to prepare a test
+setup, some of which are use case specific.
+
+### Tracepoints {#tracepoints}
+
+Applications can instrument their code with the
+[androidx.tracing.Trace](https://developer.android.com/reference/kotlin/androidx/tracing/Trace)
+class. It is strongly recommended to instrument key workloads in your
+application to increase the utility of traces, both for local profiling, and for
+inspecting results from CI. In Kotlin, the `androidx.tracing:tracing-ktx` module
+[makes this very simple](https://developer.android.com/reference/kotlin/androidx/tracing/package-summary?hl=en#trace\(kotlin.String,%20kotlin.Function0\)):
+
+```kotlin
+fun loadItemData(configuration: Config) = trace("loadItemData") {
+ // perform expensive operation here ...
+}
+```
+
+While traces are being captured, tracing does incur a small overhead (roughly
+5us) per section, so don't put it around every method. Just tracing larger
+chunks of work (>0.1ms) can give significant insights into bottlenecks.
+
+### APK Considerations {#apk-considerations}
+
+**Do not measure performance on a
+[debug build](https://developer.android.com/studio/debug).**
+
+Debug variants can be helpful for troubleshooting and symbolizing stack samples,
+but they have severe non-linear impacts on performance. Devices on Q+ can use
+_[profileableFromShell](https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/android_application_profiling.md)_
+in their manifest to enable profiling in release builds.
+
+Use your production grade
+[proguard](https://developer.android.com/studio/build/shrink-code)
+configuration. Depending on the resources your application utilizes, this can
+have a substantial impact on performance. Note that some proguard configs strip
+tracepoints, consider removing those rules for the configuration you're running
+tests on
+
+#### Compilation
+
+[Compile](https://source.android.com/devices/tech/dalvik/configure#system_rom)
+your application on-device to a known state (generally speed or speed-profile).
+Background JIT activity can have a significant performance overhead, and you
+will hit it often if you are reinstalling the APK between test runs. The command
+to do this is:
+
+```shell
+adb shell cmd package compile -m speed -f com.google.packagename
+```
+
+The 'speed' compilation mode will compile the app completely; the
+'speed-profile' mode will compile the app according to a profile of the utilized
+code paths that is collected during app usage. It can be a bit tricky to collect
+profiles consistently and correctly, so if you decide to use them, you'll
+probably want to confirm they are what you expect. They're located here:
+
+```shell
+/data/misc/profiles/ref/[package-name]/primary.prof
+```
+
+Note that Macrobenchmark allows you to directly
+[specify compilation mode](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/benchmark/docs/macrobenchmark.md#compilation-mode).
+
+### System Considerations {#system-considerations}
+
+For low-level/high fidelity measurements, **calibrate your devices**. Try to run
+A/B comparisons across the same device and same OS version. There can be
+significant variations in performance, even across the same device type.
+
+On rooted devices, consider using a
+[lockClocks script](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:benchmark/gradle-plugin/src/main/resources/scripts/lockClocks.sh)
+for microbenchmarks. Among other things, these scripts: place CPUs at a fixed
+frequency, disable little cores, configure the GPU, and disable thermal
+throttling. This is not recommended for user-experience focused tests (i.e app
+launch, DoU testing, jank testing, etc.), but can be essential for cutting down
+noise in microbenchmark tests.
+
+On rooted devices, consider killing the application and dropping file caches
+(“echo 3 > /proc/sys/vm/drop\_caches”) between iterations. This will more
+accurately model cold start behavior.
+
+When possible, consider using a testing framework like
+[Macrobenchmark](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/benchmark/docs/macrobenchmark.md),
+which can reduce noise in your measurements, and prevent measurement inaccuracy.
+
+## Common Problems {#common-problems}
+
+### Slow App Startup: Unnecessary Trampoline Activity {#slow-app-startup-unnecessary-trampoline-activity}
+
+A trampoline activity can extend app startup time unnecessarily, and it's
+important to be aware if your app is doing it. As you can see in the example
+trace below, one activityStart is immediately followed by another activityStart
+without any frames being drawn by the first activity.
+
+![alt_text](app_audit_images/trace_startup.png "Trace showing trampoline activity.")
+
+This can happen both in a notification entrypoint and a regular app startup
+entrypoint, and can often be addressed by refactoring -- can you add the check
+you're firing up an activity for as a module that can be shared between
+activities?
+
+### Unnecessary Allocations Triggering Frequent GCs {#unnecessary-allocations-triggering-frequent-gcs}
+
+You may note that GCs are happening more frequently than you expect in a
+systrace.
+
+In this case, every 10 seconds during a long-running operation is an indicator
+that we might be allocating unnecessarily but consistently over time:
+
+![alt_text](app_audit_images/trace_gc.png "Trace showing space between GC events.")
+
+Or, you may notice that a specific callstack is making the vast majority of the
+allocations when using the Memory Profiler. You don't need to eliminate all
+allocations aggressively, as this can make code harder to maintain. Start
+instead by working on hotspots of allocations.
+
+### Janky Frames {#janky-frames}
+
+The graphics pipeline is relatively complicated, and there can be some nuance
+involved in determining whether a user ultimately may have seen a dropped
+frame - in some cases, the platform can "rescue" a frame using buffering.
+However, you can ignore most of that nuance to easily identify problematic
+frames from your app's perspective.
+
+When frames are being drawn with little work required from the app, the
+Choreographer#doFrame tracepoints occur on a 16.7ms cadence (assuming a 60 FPS
+device):
+
+![alt_text](app_audit_images/trace_frames1.png "Trace showing frequent fast frames.")
+
+If you zoom out and navigate through the trace, you'll sometimes see frames take
+a little longer to complete, but that's still okay because they're not taking
+more than their allotted 16.7ms time:
+
+![alt_text](app_audit_images/trace_frames2.png "Trace showing frequent fast frames with periodic bursts of work.")
+
+But when you actually see a disruption to that regular cadence, that will be a
+janky frame:
+
+![alt_text](app_audit_images/trace_frames3.png "Trace showing janky frames.")
+
+With a little practice, you'll be able to see them everywhere!
+
+![alt_text](app_audit_images/trace_frames4.png "Trace showing more janky frames.")
+
+In some cases, you'll just need to zoom into that tracepoint for more
+information about which views are being inflated or what RecyclerView is doing.
+In other cases, you may have to inspect further.
+
+For more information about identifying janky frames and debugging their causes,
+see
+
+[Slow Rendering Vitals Documentation](https://developer.android.com/topic/performance/vitals/render)
+
+### Common RecyclerView mistakes {#common-recyclerview-mistakes}
+
+* Invalidating the entire RecyclerView's backing data unnecessarily. This can
+ lead to long frame rendering times and hence a jank. Instead, invalidate
+ only the data that has changed, to minimize the number of views that need to
+ update.
+ * See
+ [Presenting Dynamic Data](https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView#presenting-dynamic-data)
+ for ways to avoid costly notifyDatasetChanged() calls, when content is
+ updated rather than replaced from scratch.
+* Failing to support nested RecyclerViews properly, causing the internal
+ RecyclerView to be re-created from scratch every time.
+ * Every nested, inner RecyclerView should have a
+ [RecycledViewPool](https://developer.android.com/reference/kotlin/androidx/recyclerview/widget/RecyclerView.RecycledViewPool)
+ set to ensure Views can be recycled between inner RecyclerViews
+* Not prefetching enough data, or not prefetching in a timely manner. It can
+ be jarring to quickly hit the bottom of a scrolling list and need to wait
+ for more data from the server. While this isn't technically "jank" as no
+ frame deadlines are being missed, it can be a significant UX improvement
+ just to tweak the timing and quantity of prefetching so that the user
+ doesn't have to wait for data.
+
+## Tools Documentation {#tools-documentation}
+
+### Using Systrace/Perfetto {#using-systrace-perfetto}
+
+In case you like videos,
+[here's a talk](https://www.youtube.com/watch?v=qXVxuLvzKek) summarizing basic
+systrace usage.
+
+#### Debugging App Startup Using Systrace {#debugging-app-startup-using-systrace}
+
+The
+[Android developer documentation on App startup time](https://developer.android.com/topic/performance/vitals/launch-time)
+provides a good overview of the application startup process.
+
+Generally the stages of app startup are:
+
+* Launch the process
+* Initialize generic application objects
+* Create and Initialize activity
+* Inflate the layout
+* Draw the first frame
+
+Startup types can be disambiguated by these stages:
+
+* Cold startup -- Start at creating a new process with no
+ [saved state](https://developer.android.com/reference/android/os/Bundle).
+* Warm startup -- Either recreates the activity while reusing the process, or
+ recreates the process with saved state. The Macrobenchmark testing library
+ supports consistent warm startup testing utilizing the first option.
+* Hot startup -- Restarts the activity and starts at inflation.
+
+We recommend capturing systraces
+[using the on-device system tracing app available in Developer Options](https://developer.android.com/topic/performance/tracing/on-device).
+If you'd like to use command-line tools, [Perfetto](http://perfetto.dev/docs) is
+available on Android Q+, while devices on earlier versions should rely on
+[systrace](https://developer.android.com/topic/performance/vitals/launch-time).
+
+Note that “first frame” is a bit of a misnomer, applications can vary
+significantly in how they handle startup after creating the initial activity.
+Some applications will continue inflation for several frames, and others will
+even immediately launch into a secondary activity.
+
+When possible, we recommend that app developers include a
+[reportFullyDrawn](https://developer.android.com/reference/android/app/Activity#reportFullyDrawn\(\))
+(available Q+) call when startup is completed from the application’s
+perspective. RFD-defined start times can be extracted through the
+[Perfetto trace processor](https://perfetto.dev/docs/analysis/trace-processor),
+and a user-visible trace event will be emitted.
+
+Some things that you should look for include:
+
+* [Monitor contention](app_audit_images/trace_monitor_contention.png) --
+ competition for monitor-protected resources can introduce significant delay
+ in app startup.
+* [Synchronous binder transactions](app_audit_images/trace_sync_binder.png) --
+ Look for unnecessary transactions in your application’s critical path.
+* [Common Sources of Jank](https://developer.android.com/topic/performance/vitals/render#common-jank)
+* [Concurrent garbage collection](app_audit_images/trace_concurrent_gc.png) is
+ common and relatively low impact, but if you’re hitting it often consider
+ looking into it with the Android Studio memory profiler.
+* Check for [IO](app_audit_images/trace_uninterruptable_sleep.png) performed
+ during startup, and look for long stalls.
+ * Note that other processes performing IO at the same time can cause IO
+ contention, so ensure that other processes are not running.
+* Significant activity on other threads can interfere with the UI thread, so
+ watch out for background work during startup. Note that devices can have
+ different CPU configurations, so the number of threads that can run in
+ parallel can vary across devices.
+
+### Using Android Studio Memory Profiler {#using-android-studio-memory-profiler}
+
+[Memory profiler documentation](https://developer.android.com/studio/profile/memory-profiler)
+
+The Android Studio memory profiler is a powerful tool to reduce memory pressure
+that could be caused by memory leaks or bad usage patterns since it provides a
+live view of object allocations
+
+To fix memory problems in your app you can use the memory profiler to track why
+and how often garbage collections happen.
+
+The overall steps taken when profiling app memory can be broken down into the
+following steps:
+
+#### 1. Detect memory problems
+
+Start recording a memory profiling session of the user journey you care about,
+then look for an
+[increasing object count](app_audit_images/studio_increasing_object_count.jpg)
+which will eventually lead to
+[garbage collections](app_audit_images/studio_gc.jpg).
+
+Once you have identified that there is a certain user journey that is adding
+memory pressure start analyzing for root causes of the memory pressure.
+
+#### 2. Diagnose memory pressure hot spots
+
+Select a range in the timeline to
+[visualize both Allocations and Shallow Size](app_audit_images/studio_alloc_and_shallow_size.jpg).
+The are multiple ways to sort this data. Here are some examples of how each view
+can help you analyze problems.
+
+##### Arrange by class
+
+Useful when you want to find classes that are generating objects that should
+otherwise be cached or reused from a memory pool.
+
+For example: Imagine you see an app creating 2000 objects of class called
+“Vertex” every second. This would increase the Allocations count by 2000 every
+second and you would see it when sorting by class. Should we be reusing such
+objects to avoid generating that garbage? If the answer is yes, then likely
+implementing a memory pool will be needed.
+
+##### Arrange by callstack
+
+Useful when there is a hot path where memory is being allocated, maybe inside a
+for loop or inside a specific function doing a lot of allocation work you will
+be able to find it here.
+
+##### Shallow size vs Retained size, which one should I use to find memory issues?
+
+Shallow size only tracks the memory of the object itself, so it will be useful
+for tracking simple classes composed mostly of primitive values only.
+
+Retained Size shows the total memory due to the object and references that are
+solely referenced by the object, so it will be useful for tracking memory
+pressure due to complex objects. To get this value, take a
+[full memory dump](app_audit_images/studio_memory_dump.jpg) and it will be
+[added as a column](app_audit_images/studio_retained_size.jpg).
+
+### 3. Measure impact of an optimization
+
+The more evident and easy to measure impact of memory optimizations are GCs.
+When an optimization reduces the memory pressure, then you should see fewer GCs.
+
+To measure this, in the profiler timeline measure the time between GCs, and you
+should see it taking longer between GCs.
+
+The ultimate impact of memory improvements like this is:
+
+* OOM kills will likely be reduced if the app does not constantly hit memory
+ pressure
+* Having less GCs improves jank metrics, especially in the P99. This is
+ because GCs cause CPU contention, which could lead to rendering tasks being
+ deferred while GC is happening.
diff --git a/benchmark/docs/app_audit_images/studio_alloc_and_shallow_size.jpg b/benchmark/docs/app_audit_images/studio_alloc_and_shallow_size.jpg
new file mode 100644
index 0000000..49a839d
--- /dev/null
+++ b/benchmark/docs/app_audit_images/studio_alloc_and_shallow_size.jpg
Binary files differ
diff --git a/benchmark/docs/app_audit_images/studio_gc.jpg b/benchmark/docs/app_audit_images/studio_gc.jpg
new file mode 100644
index 0000000..b6d4162
--- /dev/null
+++ b/benchmark/docs/app_audit_images/studio_gc.jpg
Binary files differ
diff --git a/benchmark/docs/app_audit_images/studio_increasing_object_count.jpg b/benchmark/docs/app_audit_images/studio_increasing_object_count.jpg
new file mode 100644
index 0000000..b8da923
--- /dev/null
+++ b/benchmark/docs/app_audit_images/studio_increasing_object_count.jpg
Binary files differ
diff --git a/benchmark/docs/app_audit_images/studio_memory_dump.jpg b/benchmark/docs/app_audit_images/studio_memory_dump.jpg
new file mode 100644
index 0000000..6c45bcd
--- /dev/null
+++ b/benchmark/docs/app_audit_images/studio_memory_dump.jpg
Binary files differ
diff --git a/benchmark/docs/app_audit_images/studio_retained_size.jpg b/benchmark/docs/app_audit_images/studio_retained_size.jpg
new file mode 100644
index 0000000..d3f4f66
--- /dev/null
+++ b/benchmark/docs/app_audit_images/studio_retained_size.jpg
Binary files differ
diff --git a/benchmark/docs/app_audit_images/trace_concurrent_gc.png b/benchmark/docs/app_audit_images/trace_concurrent_gc.png
new file mode 100644
index 0000000..9e640db
--- /dev/null
+++ b/benchmark/docs/app_audit_images/trace_concurrent_gc.png
Binary files differ
diff --git a/benchmark/docs/app_audit_images/trace_frames1.png b/benchmark/docs/app_audit_images/trace_frames1.png
new file mode 100644
index 0000000..b4d6397
--- /dev/null
+++ b/benchmark/docs/app_audit_images/trace_frames1.png
Binary files differ
diff --git a/benchmark/docs/app_audit_images/trace_frames2.png b/benchmark/docs/app_audit_images/trace_frames2.png
new file mode 100644
index 0000000..9c1a54d
--- /dev/null
+++ b/benchmark/docs/app_audit_images/trace_frames2.png
Binary files differ
diff --git a/benchmark/docs/app_audit_images/trace_frames3.png b/benchmark/docs/app_audit_images/trace_frames3.png
new file mode 100644
index 0000000..0c66bb6
--- /dev/null
+++ b/benchmark/docs/app_audit_images/trace_frames3.png
Binary files differ
diff --git a/benchmark/docs/app_audit_images/trace_frames4.png b/benchmark/docs/app_audit_images/trace_frames4.png
new file mode 100644
index 0000000..3d9b145
--- /dev/null
+++ b/benchmark/docs/app_audit_images/trace_frames4.png
Binary files differ
diff --git a/benchmark/docs/app_audit_images/trace_gc.png b/benchmark/docs/app_audit_images/trace_gc.png
new file mode 100644
index 0000000..01b254b
--- /dev/null
+++ b/benchmark/docs/app_audit_images/trace_gc.png
Binary files differ
diff --git a/benchmark/docs/app_audit_images/trace_monitor_contention.png b/benchmark/docs/app_audit_images/trace_monitor_contention.png
new file mode 100644
index 0000000..af66d1e
--- /dev/null
+++ b/benchmark/docs/app_audit_images/trace_monitor_contention.png
Binary files differ
diff --git a/benchmark/docs/app_audit_images/trace_startup.png b/benchmark/docs/app_audit_images/trace_startup.png
new file mode 100644
index 0000000..4c75837
--- /dev/null
+++ b/benchmark/docs/app_audit_images/trace_startup.png
Binary files differ
diff --git a/benchmark/docs/app_audit_images/trace_sync_binder.png b/benchmark/docs/app_audit_images/trace_sync_binder.png
new file mode 100644
index 0000000..4920f17
--- /dev/null
+++ b/benchmark/docs/app_audit_images/trace_sync_binder.png
Binary files differ
diff --git a/benchmark/docs/app_audit_images/trace_uninterruptable_sleep.png b/benchmark/docs/app_audit_images/trace_uninterruptable_sleep.png
new file mode 100644
index 0000000..b305a9b
--- /dev/null
+++ b/benchmark/docs/app_audit_images/trace_uninterruptable_sleep.png
Binary files differ
diff --git a/benchmark/docs/macrobenchmark.md b/benchmark/docs/macrobenchmark.md
index 21fb73d..3330d59a 100644
--- a/benchmark/docs/macrobenchmark.md
+++ b/benchmark/docs/macrobenchmark.md
@@ -4,6 +4,8 @@
[developer.android.com](https://developer.android.com) when the library is ready
for public release.
+NOTE: See [Known Issues](#known-issues) for workarounds for library issues.
+
[TOC]
## Jetpack Macrobenchmark
@@ -49,8 +51,6 @@
you're testing (e.g. you can specify cold, first-time install startup
performance).
-
-
<!-- Below table doesn't work on android.googlesource.com, using image as workaround
<table>
<tr>
@@ -140,6 +140,19 @@
preferably with minification on (which is beneficial for performance). This is
typically done by installing the `release` variant of the target apk.
+As it is necessary to sign your app's `release` variant before building, you can
+sign it locally with `debug` keys:
+
+```groovy
+ buildTypes {
+ release {
+ // You'll be unable to release with this config, but it can
+ // be useful for local performance testing
+ signingConfig signingConfigs.debug
+ }
+ }
+```
+
Every Activity to be launched by a Macrobenchmark must
[exported](https://developer.android.com/guide/topics/manifest/activity-element#exported).
As of Android 11, this must be enabled explicitly in the app's
@@ -151,8 +164,8 @@
android:exported="true">
```
-The target application must also be profileable, to enable reading detailed
-trace information. This is enabled in the `<application>` tag of the app's
+Your app will also need to be profileable, to enable reading detailed trace
+information. This is enabled in the `<application>` tag of the app's
AndroidManifest.xml:
```xml
@@ -175,13 +188,6 @@
## Run the Macrobenchmark
-```
-Some users are experiencing issues running macro benchmarks on certain devices and OS versions, especially non-Pixel devices.
-If you run into an issue please [report feedback](#feedback), with the exception and a logcat snippet from the test run.
-
-If you hit a problem, try a different test device as a temporary workaround.
-```
-
Run the test from within Studio to measure the performance of your app on your
device. Note that you **must run the test on a physical device**, and not an
emulator, as emulators do not produce performance numbers representative of
@@ -294,10 +300,19 @@
```shell
# the following command will pull all files ending in .trace
-adb shell 'ls /storage/emulated/0/Download/*.trace' \
+# if you have not overriden the output directory
+adb shell ls '/storage/emulated/0/Android/data/*/*/*.trace' \
| tr -d '\r' | xargs -n1 adb pull
```
+Note that your output file path may be different if you customize it with the
+`additionalTestOutputDir` argument. You can look for trace path logs in logcat
+to see where there are written, for example:
+
+```
+I PerfettoCapture: Writing to /storage/emulated/0/Android/data/androidx.benchmark.integration.macrobenchmark.test/cache/TrivialStartupBenchmark_startup[mode=COLD]_iter002.trace.
+```
+
If you invoke the tests instead via gradle command line (e.g. `./gradlew
macrobenchmark:connectedCheck`), these files are pulled automatically to a test
output directory:
@@ -373,6 +388,12 @@
</manifest>
```
+Or pass an instrumentation arg to bypass scoped storage for the test:
+
+```shell
+-e no-isolated-storage 1
+```
+
NOTE: The file extension of these trace files is currently `.trace`, but will
likely change in the future to clarify that these are perfetto traces.
@@ -434,6 +455,17 @@
For guidance in how to detect performance regressions, see the blogpost
[Fighting Regressions with Benchmarks in CI](https://medium.com/androiddevelopers/fighting-regressions-with-benchmarks-in-ci-6ea9a14b5c71).
+
+## Known Issues
+
+### Missing Metrics
+
+If you see exceptions with the text: `Unable to read any metrics during
+benchmark` or `Error, different metrics observed in different iterations.` in a
+startup benchmark, these can be caused by the library failing to wait for
+Activity launch. As a temporary workaround, you can add a
+`Thread.sleep(5000/*ms*/)` at the end of your `measureRepeated {}` block.
+
## Feedback
To report issues or submit feature requests for Jetpack Macrobenchmark, see the
diff --git a/benchmark/integration-tests/macrobenchmark-target/lint-baseline.xml b/benchmark/integration-tests/macrobenchmark-target/lint-baseline.xml
deleted file mode 100644
index 8f1aa4b..0000000
--- a/benchmark/integration-tests/macrobenchmark-target/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/benchmark/integration-tests/macrobenchmark/lint-baseline.xml b/benchmark/integration-tests/macrobenchmark/lint-baseline.xml
deleted file mode 100644
index 8f1aa4b..0000000
--- a/benchmark/integration-tests/macrobenchmark/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/benchmark/junit4/lint-baseline.xml b/benchmark/junit4/lint-baseline.xml
deleted file mode 100644
index 8f1aa4b..0000000
--- a/benchmark/junit4/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/benchmark/macro-junit4/lint-baseline.xml b/benchmark/macro-junit4/lint-baseline.xml
deleted file mode 100644
index 8f1aa4b..0000000
--- a/benchmark/macro-junit4/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/benchmark/macro-junit4/src/main/java/androidx/benchmark/macro/junit4/PerfettoRule.kt b/benchmark/macro-junit4/src/main/java/androidx/benchmark/macro/junit4/PerfettoRule.kt
index 2891995..3ba31d4 100644
--- a/benchmark/macro-junit4/src/main/java/androidx/benchmark/macro/junit4/PerfettoRule.kt
+++ b/benchmark/macro-junit4/src/main/java/androidx/benchmark/macro/junit4/PerfettoRule.kt
@@ -19,8 +19,7 @@
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
-import androidx.benchmark.Arguments
-import androidx.benchmark.InstrumentationResults
+import androidx.benchmark.Outputs
import androidx.benchmark.macro.perfetto.PerfettoCapture
import org.junit.rules.TestRule
import org.junit.runner.Description
@@ -74,10 +73,11 @@
Log.d(PerfettoRule.TAG, "Recording perfetto trace $traceName")
start()
block()
- val destinationPath = Arguments.testOutputFile(traceName).absolutePath
- stop(destinationPath)
- Log.d(PerfettoRule.TAG, "Finished recording to $destinationPath")
- InstrumentationResults.reportAdditionalFileToCopy("perfetto_trace", destinationPath)
+ Outputs.writeFile(fileName = traceName, reportKey = "perfetto_trace") {
+ val destinationPath = it.absolutePath
+ stop(destinationPath)
+ Log.d(PerfettoRule.TAG, "Finished recording to $destinationPath")
+ }
} finally {
cancel()
}
diff --git a/benchmark/macro/lint-baseline.xml b/benchmark/macro/lint-baseline.xml
deleted file mode 100644
index 8f1aa4b..0000000
--- a/benchmark/macro/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/IdeSummaryStringTest.kt b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/IdeSummaryStringTest.kt
index 58cdc4f..b74eca1 100644
--- a/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/IdeSummaryStringTest.kt
+++ b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/IdeSummaryStringTest.kt
@@ -16,13 +16,14 @@
package androidx.benchmark.macro
-import androidx.benchmark.Arguments
+import androidx.benchmark.Outputs
import androidx.benchmark.Stats
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
+import java.io.File
import kotlin.test.assertFailsWith
@RunWith(AndroidJUnit4::class)
@@ -31,7 +32,7 @@
private fun createAbsoluteTracePaths(
@Suppress("SameParameterValue") count: Int
) = List(count) {
- Arguments.testOutputFile("iter$it.trace").path
+ File(Outputs.dirUsableByAppAndShell, "iter$it.trace").absolutePath
}
@Test
@@ -143,4 +144,4 @@
).first
}
}
-}
\ No newline at end of file
+}
diff --git a/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureTest.kt b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureTest.kt
index 8e6513c..ffa8939 100644
--- a/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureTest.kt
+++ b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureTest.kt
@@ -16,10 +16,10 @@
package androidx.benchmark.macro.perfetto
+import androidx.benchmark.Outputs
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
-import androidx.test.platform.app.InstrumentationRegistry
import androidx.testutils.verifyWithPolling
import androidx.tracing.Trace
import androidx.tracing.trace
@@ -33,8 +33,7 @@
@SdkSuppress(minSdkVersion = 29)
@RunWith(AndroidJUnit4::class)
class PerfettoCaptureTest {
- private val context = InstrumentationRegistry.getInstrumentation().context
- private val traceFile = File(context.getExternalFilesDir(null), "PerfettoCaptureTest.trace")
+ private val traceFile = File(Outputs.dirUsableByAppAndShell, "PerfettoCaptureTest.trace")
private val traceFilePath = traceFile.absolutePath
@Before
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/IdeSummaryString.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/IdeSummaryString.kt
index 5b797fe..2b79b2e 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/IdeSummaryString.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/IdeSummaryString.kt
@@ -16,7 +16,7 @@
package androidx.benchmark.macro
-import androidx.benchmark.Arguments
+import androidx.benchmark.Outputs
import androidx.benchmark.Stats
import java.util.Collections
import kotlin.math.max
@@ -61,11 +61,8 @@
} + "\n"
}
val relativeTracePaths = absoluteTracePaths.map { absolutePath ->
- val relativePath = absolutePath.removePrefix(Arguments.outputDirectoryPath + "/")
- check(relativePath != absolutePath)
- relativePath
+ Outputs.relativePathFor(absolutePath)
}
-
return Pair(
first = ideSummaryString { name, min, median, max, _ ->
" $name min $min, median $median, max $max"
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoCapture.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoCapture.kt
index 267d8c0..6bba7c4 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoCapture.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoCapture.kt
@@ -18,6 +18,7 @@
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
+import androidx.benchmark.Outputs
import androidx.benchmark.macro.R
import androidx.test.platform.app.InstrumentationRegistry
import java.io.File
@@ -55,7 +56,7 @@
// Write textproto asset to external files dir, so it can be read by shell
// TODO: use binary proto (which will also give us rooted 28 support)
val configBytes = context.resources.openRawResource(R.raw.trace_config).readBytes()
- val textProtoFile = File(context.getExternalFilesDir(null), "trace_config.textproto")
+ val textProtoFile = File(Outputs.dirUsableByAppAndShell, "trace_config.textproto")
try {
textProtoFile.writeBytes(configBytes)
helper.startCollecting(textProtoFile.absolutePath, true)
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoCaptureWrapper.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoCaptureWrapper.kt
index 89b50db..d1f8f89 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoCaptureWrapper.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoCaptureWrapper.kt
@@ -19,8 +19,7 @@
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
-import androidx.benchmark.Arguments
-import androidx.benchmark.InstrumentationResults
+import androidx.benchmark.Outputs
import androidx.benchmark.macro.device
import androidx.test.platform.app.InstrumentationRegistry
@@ -49,19 +48,15 @@
@RequiresApi(Build.VERSION_CODES.Q)
private fun stop(benchmarkName: String, iteration: Int): String {
val iterString = iteration.toString().padStart(3, '0')
- // NOTE: macrobench still using legacy .trace name until
+ // NOTE: Macrobenchmarks still use legacy .trace name until
// Studio supports .perfetto-trace extension (b/171251272)
val traceName = "${benchmarkName}_iter$iterString.trace".replace(
oldValue = " ",
newValue = ""
)
- val destination = Arguments.testOutputFile(traceName).absolutePath
- capture!!.stop(destination)
- InstrumentationResults.reportAdditionalFileToCopy(
- key = "perfetto_trace_$iterString",
- absoluteFilePath = destination
- )
- return destination
+ return Outputs.writeFile(fileName = traceName, reportKey = "perfetto_trace_$iterString") {
+ capture!!.stop(it.absolutePath)
+ }
}
fun record(
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/perfetto/ShellUtils.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/perfetto/ShellUtils.kt
index bba9b68..11d1f00c 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/perfetto/ShellUtils.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/perfetto/ShellUtils.kt
@@ -16,7 +16,7 @@
package androidx.benchmark.macro.perfetto
-import androidx.test.platform.app.InstrumentationRegistry
+import androidx.benchmark.Outputs
import androidx.test.uiautomator.UiDevice
import java.io.File
import java.io.InputStream
@@ -36,11 +36,9 @@
* @param stdin String to pass in as stdin to first command in script
*/
fun UiDevice.executeShellScript(script: String, stdin: String? = null): String {
- // externalFilesDir is writable, but we can't execute there (as of Q),
+ // dirUsableByAppAndShell is writable, but we can't execute there (as of Q),
// so we copy to /data/local/tmp
- val context = InstrumentationRegistry.getInstrumentation().context
- val externalDir = context.getExternalFilesDir(null)!!
-
+ val externalDir = Outputs.dirUsableByAppAndShell
val stdinFile = File.createTempFile("temporaryStdin", null, externalDir)
val writableScriptFile = File.createTempFile("temporaryScript", "sh", externalDir)
val runnableScriptPath = "/data/local/tmp/" + writableScriptFile.name
@@ -70,11 +68,9 @@
* Writes the inputStream to an executable file with the given name in `/data/local/tmp`
*/
fun UiDevice.createRunnableExecutable(name: String, inputStream: InputStream): String {
- // externalFilesDir is writable, but we can't execute there (as of Q),
+ // dirUsableByAppAndShell is writable, but we can't execute there (as of Q),
// so we copy to /data/local/tmp
- val context = InstrumentationRegistry.getInstrumentation().context
- val externalDir = context.getExternalFilesDir(null)!!
-
+ val externalDir = Outputs.dirUsableByAppAndShell
val writableExecutableFile = File.createTempFile(
/* prefix */ "temporary_$name",
/* suffix */ null,
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilderTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilderTest.kt
index 5b8f59b..4798cfc 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilderTest.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilderTest.kt
@@ -162,7 +162,7 @@
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.ShippingApiLevelModuleController">
+ <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" />
@@ -193,7 +193,7 @@
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.ShippingApiLevelModuleController">
+ <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" />
diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXPlaygroundRootPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXPlaygroundRootPlugin.kt
index b97e4d7..aa96978 100644
--- a/buildSrc/src/main/kotlin/androidx/build/AndroidXPlaygroundRootPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXPlaygroundRootPlugin.kt
@@ -16,6 +16,8 @@
package androidx.build
+import androidx.build.AndroidXRootPlugin.Companion.PREBUILT_OR_SNAPSHOT_EXT_NAME
+import androidx.build.AndroidXRootPlugin.Companion.PROJECT_OR_ARTIFACT_EXT_NAME
import androidx.build.gradle.isRoot
import groovy.util.XmlParser
import groovy.xml.QName
@@ -52,6 +54,12 @@
}
)
+ private val prebuiltOrSnapshotClosure = KotlinClosure1<String, String>(
+ function = {
+ prebuiltOrSnapshot(this)
+ }
+ )
+
override fun apply(target: Project) {
if (!target.isRoot) {
throw GradleException("This plugin should only be applied to root project")
@@ -72,7 +80,8 @@
private fun configureSubProject(project: Project) {
project.repositories.addPlaygroundRepositories()
- project.extra.set(AndroidXRootPlugin.PROJECT_OR_ARTIFACT_EXT_NAME, projectOrArtifactClosure)
+ project.extra.set(PROJECT_OR_ARTIFACT_EXT_NAME, projectOrArtifactClosure)
+ project.extra.set(PREBUILT_OR_SNAPSHOT_EXT_NAME, prebuiltOrSnapshotClosure)
project.configurations.all { configuration ->
configuration.resolutionStrategy.dependencySubstitution.all { substitution ->
substitution.allowAndroidxSnapshotReplacement()
@@ -118,6 +127,21 @@
}
}
+ private fun prebuiltOrSnapshot(path: String): String {
+ val sections = path.split(":")
+
+ if (sections.size != 3) {
+ throw GradleException(
+ "Expected prebuiltOrSnapshot path to be of the form " +
+ "<group>:<artifact>:<version>, but was $path"
+ )
+ }
+
+ val group = sections[0]
+ val artifact = sections[1]
+ return "$group:$artifact:$SNAPSHOT_MARKER"
+ }
+
private fun DependencySubstitution.allowAndroidxSnapshotReplacement() {
val requested = this.requested
if (requested is ModuleComponentSelector && requested.group.startsWith("androidx") &&
diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt
index 6420a50..1d82fb3 100644
--- a/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt
@@ -101,6 +101,15 @@
}
)
)
+ project.extra.set(
+ PREBUILT_OR_SNAPSHOT_EXT_NAME,
+ KotlinClosure1<String, String>(
+ function = {
+ // this refers to the first parameter of the closure.
+ this
+ }
+ )
+ )
project.plugins.withType(AndroidBasePlugin::class.java) {
buildOnServerTask.dependsOn("${project.path}:assembleDebug")
buildOnServerTask.dependsOn("${project.path}:assembleAndroidTest")
@@ -226,5 +235,6 @@
companion object {
const val PROJECT_OR_ARTIFACT_EXT_NAME = "projectOrArtifact"
+ const val PREBUILT_OR_SNAPSHOT_EXT_NAME = "prebuiltOrSnapshot"
}
}
diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXUiPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXUiPlugin.kt
index 0c75234..aa2a2be 100644
--- a/buildSrc/src/main/kotlin/androidx/build/AndroidXUiPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXUiPlugin.kt
@@ -140,17 +140,20 @@
// Too many Kotlin features require synthetic accessors - we want to rely on R8 to
// remove these accessors
disable("SyntheticAccessor")
- // Composable naming is normally a warning, but we ignore (in AndroidX)
+ // These lint checks are normally a warning (or lower), but we ignore (in AndroidX)
// warnings in Lint, so we make it an error here so it will fail the build.
// Note that this causes 'UnknownIssueId' lint warnings in the build log when
- // Lint tries to apply this rule to modules that do not have this lint check.
- // Unfortunately suppressing this doesn't seem to work, and disabling it causes
- // it just to log `Lint: Unknown issue id "ComposableNaming"`, which will still
- // cause the build log simplifier to fail.
+ // Lint tries to apply this rule to modules that do not have this lint check, so
+ // we disable that check too
+ disable("UnknownIssueId")
error("ComposableNaming")
error("ComposableLambdaParameterNaming")
error("ComposableLambdaParameterPosition")
error("CompositionLocalNaming")
+ error("ComposableModifierFactory")
+ error("ModifierFactoryReturnType")
+ error("ModifierFactoryExtensionFunction")
+ error("ModifierParameter")
}
// TODO(148540713): remove this exclusion when Lint can support using multiple lint jars
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index b53880c..5953be2 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -22,7 +22,7 @@
object LibraryVersions {
val ACTIVITY = Version("1.3.0-alpha04")
val ADS_IDENTIFIER = Version("1.0.0-alpha04")
- val ANNOTATION = Version("1.2.0-rc01")
+ val ANNOTATION = Version("1.3.0-alpha01")
val ANNOTATION_EXPERIMENTAL = Version("1.1.0-rc01")
val APPCOMPAT = Version("1.3.0-beta02")
val APPSEARCH = Version("1.0.0-alpha01")
@@ -144,7 +144,7 @@
val WEAR_WATCHFACE_EDITOR = Version("1.0.0-alpha09")
val WEAR_WATCHFACE_STYLE = Version("1.0.0-alpha09")
val WEBKIT = Version("1.5.0-alpha01")
- val WINDOW = Version("1.0.0-alpha04")
+ val WINDOW = Version("1.0.0-alpha05")
val WINDOW_EXTENSIONS = Version("1.0.0-alpha01")
val WINDOW_SIDECAR = Version("0.1.0-alpha01")
val WORK = Version("2.6.0-alpha01")
diff --git a/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
index 4ce1461..6c4446d 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -159,6 +159,11 @@
disable("BanUncheckedReflection")
}
+ // Only run certain checks where API tracking is important.
+ if (extension.type.checkApi is RunApiTasks.No) {
+ disable("IllegalExperimentalApiUsage")
+ }
+
// If the project has not overridden the lint config, set the default one.
if (lintConfig == null) {
// suppress warnings more specifically than issue-wide severity (regexes)
diff --git a/buildSrc/src/main/kotlin/androidx/build/MavenUploadHelper.kt b/buildSrc/src/main/kotlin/androidx/build/MavenUploadHelper.kt
index 49844b1..e58490b 100644
--- a/buildSrc/src/main/kotlin/androidx/build/MavenUploadHelper.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/MavenUploadHelper.kt
@@ -32,7 +32,6 @@
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.findByType
-import org.gradle.kotlin.dsl.named
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget
import java.io.File
@@ -63,26 +62,25 @@
publications {
if (appliesJavaGradlePluginPlugin()) {
// The 'java-gradle-plugin' will also add to the 'pluginMaven' publication
- it.create<MavenPublication>("pluginMaven").pom { pom ->
- addInformativeMetadata(pom, extension)
- tweakDependenciesMetadata(extension, pom)
- }
+ it.create<MavenPublication>("pluginMaven")
tasks.getByName("publishPluginMavenPublicationToMavenRepository").doFirst {
removePreviouslyUploadedArchives(androidxGroup)
}
} else {
it.create<MavenPublication>("maven") {
from(component)
- pom { pom ->
- addInformativeMetadata(pom, extension)
- tweakDependenciesMetadata(extension, pom)
- }
}
tasks.getByName("publishMavenPublicationToMavenRepository").doFirst {
removePreviouslyUploadedArchives(androidxGroup)
}
}
}
+ publications.withType(MavenPublication::class.java).all {
+ it.pom { pom ->
+ addInformativeMetadata(extension, pom)
+ tweakDependenciesMetadata(extension, pom)
+ }
+ }
}
// Register it as part of release so that we create a Zip file for it
@@ -103,42 +101,22 @@
}
if (isMultiplatformEnabled()) {
- configureMultiplatformPublication(extension)
+ configureMultiplatformPublication()
}
}
}
-private fun Project.configureMultiplatformPublication(
- extension: AndroidXExtension
-) {
+private fun Project.configureMultiplatformPublication() {
val multiplatformExtension = extensions.findByType<KotlinMultiplatformExtension>() ?: return
// publishMavenPublicationToMavenRepository will produce conflicting artifacts with the same
// name as the artifacts producing by publishKotlinMultiplatformPublicationToMavenRepository
project.tasks.findByName("publishMavenPublicationToMavenRepository")?.enabled = false
- configure<PublishingExtension> {
- publications {
- it.named<MavenPublication>("kotlinMultiplatform") {
- pom { pom ->
- addInformativeMetadata(pom, extension)
- tweakDependenciesMetadata(extension, pom)
- }
- }
- }
- }
-
multiplatformExtension.targets.all { target ->
if (target is KotlinAndroidTarget) {
target.publishAllLibraryVariants()
}
-
- target.mavenPublication { publication ->
- publication.pom { pom ->
- addInformativeMetadata(pom, extension)
- tweakDependenciesMetadata(extension, pom)
- }
- }
}
}
@@ -169,7 +147,7 @@
projectArchiveDir.deleteRecursively()
}
-private fun Project.addInformativeMetadata(pom: MavenPom, extension: AndroidXExtension) {
+private fun Project.addInformativeMetadata(extension: AndroidXExtension, pom: MavenPom) {
pom.name.set(provider { extension.name })
pom.description.set(provider { extension.description })
pom.url.set(
diff --git a/buildSrc/src/main/kotlin/androidx/build/Release.kt b/buildSrc/src/main/kotlin/androidx/build/Release.kt
index 6cac722..75e7a472 100644
--- a/buildSrc/src/main/kotlin/androidx/build/Release.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/Release.kt
@@ -23,6 +23,7 @@
import org.gradle.api.tasks.Nested
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.bundling.Zip
+import org.gradle.plugin.devel.GradlePluginDevelopmentExtension
import java.io.File
import java.util.TreeSet
@@ -181,7 +182,7 @@
}
val version = project.version
- var zipTasks = listOf(
+ val zipTasks = listOf(
getProjectZipTask(project),
getGroupReleaseZipTask(project, mavenGroup),
getGlobalFullZipTask(project)
@@ -193,9 +194,25 @@
)
val publishTask = project.tasks.named("publish")
zipTasks.forEach {
- it.configure {
- it.candidates.add(artifact)
- it.dependsOn(publishTask)
+ it.configure { zipTask ->
+ zipTask.candidates.add(artifact)
+
+ // Add additional artifacts needed for Gradle Plugins
+ if (extension.type == LibraryType.GRADLE_PLUGIN) {
+ project.extensions.getByType(
+ GradlePluginDevelopmentExtension::class.java
+ ).plugins.forEach { plugin ->
+ zipTask.candidates.add(
+ Artifact(
+ mavenGroup = plugin.id,
+ projectName = "${plugin.id}.gradle.plugin",
+ version = version.toString()
+ )
+ )
+ }
+ }
+
+ zipTask.dependsOn(publishTask)
}
}
}
@@ -280,26 +297,20 @@
private fun getProjectZipTask(
project: Project
): TaskProvider<GMavenZipTask> {
- val taskName = "$PROJECT_ARCHIVE_ZIP_TASK_NAME"
- val taskProvider: TaskProvider<GMavenZipTask> = project.maybeRegister(
- name = taskName,
- onConfigure = {
- GMavenZipTask.ConfigAction(
- getParams(
- project = project,
- distDir = File(
- project.getDistributionDirectory(),
- PROJECT_ZIPS_FOLDER
- ),
- fileNamePrefix = project.projectZipPrefix()
- ).copy(
- includeMetadata = true
- )
- ).execute(it)
- },
- onRegister = {
- }
- )
+ val taskProvider = project.tasks.register(
+ PROJECT_ARCHIVE_ZIP_TASK_NAME,
+ GMavenZipTask::class.java
+ ) {
+ GMavenZipTask.ConfigAction(
+ getParams(
+ project = project,
+ distDir = File(project.getDistributionDirectory(), PROJECT_ZIPS_FOLDER),
+ fileNamePrefix = project.projectZipPrefix()
+ ).copy(
+ includeMetadata = true
+ )
+ ).execute(it)
+ }
project.addToBuildOnServer(taskProvider)
return taskProvider
}
diff --git a/buildSrc/src/main/kotlin/androidx/build/ReportLibraryMetricsTask.kt b/buildSrc/src/main/kotlin/androidx/build/ReportLibraryMetricsTask.kt
index 9edc2ba..bbbeffc 100644
--- a/buildSrc/src/main/kotlin/androidx/build/ReportLibraryMetricsTask.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/ReportLibraryMetricsTask.kt
@@ -31,12 +31,12 @@
import org.json.simple.JSONObject
import java.io.File
-private const val AAR_FILE_EXTENSION = ".aar"
private const val BYTECODE_SIZE = "bytecode_size"
private const val METHOD_COUNT = "method_count"
private const val METRICS_DIRECTORY = "librarymetrics"
private const val JSON_FILE_EXTENSION = ".json"
private const val JAR_FILE_EXTENSION = ".jar"
+private const val LINT_JAR = "lint$JAR_FILE_EXTENSION"
@CacheableTask
abstract class ReportLibraryMetricsTask : DefaultTask() {
@@ -77,7 +77,10 @@
private fun getJarFiles(): List<File> {
return jarFiles.files.filter { file ->
- file.name.endsWith(JAR_FILE_EXTENSION)
+ file.name.endsWith(JAR_FILE_EXTENSION) &&
+ // AARs bundle a `lint.jar` that contains lint checks published by the library -
+ // this isn't runtime code and is not part of the actual library, so ignore it.
+ file.name != LINT_JAR
}
}
diff --git a/buildSrc/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt b/buildSrc/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
index 2f70c19..5bcc7646 100644
--- a/buildSrc/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
@@ -24,6 +24,7 @@
import org.gradle.api.attributes.DocsType
import org.gradle.api.attributes.Usage
import org.gradle.api.component.AdhocComponentWithVariants
+import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.bundling.Jar
@@ -40,6 +41,8 @@
val sourceJar = tasks.register("sourceJar${variant.name.capitalize()}", Jar::class.java) {
it.archiveClassifier.set("sources")
it.from(extension.sourceSets.getByName("main").java.srcDirs)
+ // Do not allow source files with duplicate names, information would be lost otherwise.
+ it.duplicatesStrategy = DuplicatesStrategy.FAIL
}
registerSourcesVariant(sourceJar)
}
@@ -69,6 +72,8 @@
it.archiveClassifier.set("sources")
val convention = convention.getPlugin<JavaPluginConvention>()
it.from(convention.sourceSets.getByName("main").allSource.srcDirs)
+ // Do not allow source files with duplicate names, information would be lost otherwise.
+ it.duplicatesStrategy = DuplicatesStrategy.FAIL
}
registerSourcesVariant(sourceJar)
}
@@ -81,15 +86,15 @@
Usage.USAGE_ATTRIBUTE,
objects.named(Usage.JAVA_RUNTIME)
)
- gradleVariant.getAttributes().attribute(
+ gradleVariant.attributes.attribute(
Category.CATEGORY_ATTRIBUTE,
objects.named(Category.DOCUMENTATION)
)
- gradleVariant.getAttributes().attribute(
+ gradleVariant.attributes.attribute(
Bundling.BUNDLING_ATTRIBUTE,
objects.named(Bundling.EXTERNAL)
)
- gradleVariant.getAttributes().attribute(
+ gradleVariant.attributes.attribute(
DocsType.DOCS_TYPE_ATTRIBUTE,
objects.named(DocsType.SOURCES)
)
diff --git a/buildSrc/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt
index d439f0c..fdee719 100644
--- a/buildSrc/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/testConfiguration/AndroidTestXmlBuilder.kt
@@ -244,7 +244,7 @@
""".trimIndent()
private val MIN_API_LEVEL_CONTROLLER_OBJECT = """
- <object type="module_controller" class="com.android.tradefed.testtype.suite.module.ShippingApiLevelModuleController">
+ <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MinApiLevelModuleController">
<option name="min-api-level" value="MIN_SDK" />
</object>
diff --git a/camera/camera-camera2-pipe-testing/build.gradle b/camera/camera-camera2-pipe-testing/build.gradle
index 4e420b00..c340844 100644
--- a/camera/camera-camera2-pipe-testing/build.gradle
+++ b/camera/camera-camera2-pipe-testing/build.gradle
@@ -50,10 +50,6 @@
minSdkVersion 21
}
- sourceSets {
- test.java.srcDirs += 'src/robolectric/java'
- }
-
// Use Robolectric 4.+
testOptions.unitTests.includeAndroidResources = true
}
diff --git a/camera/camera-camera2-pipe-testing/src/robolectric/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt b/camera/camera-camera2-pipe-testing/src/robolectric/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
deleted file mode 100644
index ddf221c..0000000
--- a/camera/camera-camera2-pipe-testing/src/robolectric/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2.pipe.testing
-
-import org.junit.runners.model.FrameworkMethod
-import org.robolectric.RobolectricTestRunner
-import org.robolectric.internal.bytecode.InstrumentationConfiguration
-
-/**
- * A [RobolectricTestRunner] for [androidx.camera.camera2.pipe] unit tests.
- *
- * This test runner disables instrumentation for the [androidx.camera.camera2.pipe] and
- * [androidx.camera.camera2.pipe.testing] packages.
- *
- * Robolectric tries to instrument Kotlin classes, and it throws errors when it encounters
- * companion objects, constructors with default values for parameters, and data classes with
- * inline classes. We don't need shadowing of our classes because we want to use the actual
- * objects in our tests.
- */
-public class RobolectricCameraPipeTestRunner(testClass: Class<*>) :
- RobolectricTestRunner(testClass) {
- override fun createClassLoaderConfig(method: FrameworkMethod?): InstrumentationConfiguration {
- val builder = InstrumentationConfiguration.Builder(super.createClassLoaderConfig(method))
- builder.doNotInstrumentPackage("androidx.camera.camera2.pipe")
- builder.doNotInstrumentPackage("androidx.camera.camera2.pipe.testing")
- return builder.build()
- }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunnerTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
similarity index 64%
rename from camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunnerTest.kt
rename to camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
index b5f7555..13a41fc 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunnerTest.kt
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,7 +21,31 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import org.junit.runners.model.FrameworkMethod
+import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
+import org.robolectric.internal.bytecode.InstrumentationConfiguration
+
+/**
+ * A [RobolectricTestRunner] for [androidx.camera.camera2.pipe] unit tests.
+ *
+ * This test runner disables instrumentation for the [androidx.camera.camera2.pipe] and
+ * [androidx.camera.camera2.pipe.testing] packages.
+ *
+ * Robolectric tries to instrument Kotlin classes, and it throws errors when it encounters
+ * companion objects, constructors with default values for parameters, and data classes with
+ * inline classes. We don't need shadowing of our classes because we want to use the actual
+ * objects in our tests.
+ */
+public class RobolectricCameraPipeTestRunner(testClass: Class<*>) :
+ RobolectricTestRunner(testClass) {
+ override fun createClassLoaderConfig(method: FrameworkMethod?): InstrumentationConfiguration {
+ val builder = InstrumentationConfiguration.Builder(super.createClassLoaderConfig(method))
+ builder.doNotInstrumentPackage("androidx.camera.camera2.pipe")
+ builder.doNotInstrumentPackage("androidx.camera.camera2.pipe.testing")
+ return builder.build()
+ }
+}
@Suppress("EXPERIMENTAL_FEATURE_WARNING")
public inline class TestValue(public val value: String)
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCamerasTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCamerasTest.kt
deleted file mode 100644
index 87f16db..0000000
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCamerasTest.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2.pipe.testing
-
-import android.content.Context
-import android.hardware.camera2.CameraCharacteristics
-import android.os.Build
-import android.os.Looper
-import androidx.test.core.app.ApplicationProvider
-import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.robolectric.Shadows.shadowOf
-import org.robolectric.annotation.Config
-
-@RunWith(RobolectricCameraPipeTestRunner::class)
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-class RobolectricCamerasTest {
- private val context = ApplicationProvider.getApplicationContext() as Context
- private val mainLooper = shadowOf(Looper.getMainLooper())
-
- @Test
- fun fakeCamerasCanBeOpened() {
- val fakeCameraId = RobolectricCameras.create(
- mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
- )
- val fakeCamera = RobolectricCameras.open(fakeCameraId)
-
- assertThat(fakeCamera).isNotNull()
- assertThat(fakeCamera.cameraId).isEqualTo(fakeCameraId)
- assertThat(fakeCamera.cameraDevice).isNotNull()
- assertThat(fakeCamera.characteristics).isNotNull()
- assertThat(fakeCamera.characteristics[CameraCharacteristics.LENS_FACING]).isNotNull()
- assertThat(fakeCamera.metadata).isNotNull()
- assertThat(fakeCamera.metadata[CameraCharacteristics.LENS_FACING]).isNotNull()
- }
-
- @After
- fun teardown() {
- mainLooper.idle()
- RobolectricCameras.clear()
- }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/build.gradle b/camera/camera-camera2-pipe/build.gradle
index 014c031..50d5b87 100644
--- a/camera/camera-camera2-pipe/build.gradle
+++ b/camera/camera-camera2-pipe/build.gradle
@@ -61,11 +61,6 @@
minSdkVersion 21
}
- // Include additional robolectric utilities in tests
- sourceSets {
- test.java.srcDirs += ['../camera-camera2-pipe-testing/src/robolectric/java']
- }
-
// Use Robolectric 4.+
testOptions.unitTests.includeAndroidResources = true
}
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunnerTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
similarity index 64%
copy from camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunnerTest.kt
copy to camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
index b5f7555..13a41fc 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunnerTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,7 +21,31 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import org.junit.runners.model.FrameworkMethod
+import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
+import org.robolectric.internal.bytecode.InstrumentationConfiguration
+
+/**
+ * A [RobolectricTestRunner] for [androidx.camera.camera2.pipe] unit tests.
+ *
+ * This test runner disables instrumentation for the [androidx.camera.camera2.pipe] and
+ * [androidx.camera.camera2.pipe.testing] packages.
+ *
+ * Robolectric tries to instrument Kotlin classes, and it throws errors when it encounters
+ * companion objects, constructors with default values for parameters, and data classes with
+ * inline classes. We don't need shadowing of our classes because we want to use the actual
+ * objects in our tests.
+ */
+public class RobolectricCameraPipeTestRunner(testClass: Class<*>) :
+ RobolectricTestRunner(testClass) {
+ override fun createClassLoaderConfig(method: FrameworkMethod?): InstrumentationConfiguration {
+ val builder = InstrumentationConfiguration.Builder(super.createClassLoaderConfig(method))
+ builder.doNotInstrumentPackage("androidx.camera.camera2.pipe")
+ builder.doNotInstrumentPackage("androidx.camera.camera2.pipe.testing")
+ return builder.build()
+ }
+}
@Suppress("EXPERIMENTAL_FEATURE_WARNING")
public inline class TestValue(public val value: String)
diff --git a/camera/camera-camera2-pipe-testing/src/robolectric/java/androidx/camera/camera2/pipe/testing/RobolectricCameras.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameras.kt
similarity index 81%
rename from camera/camera-camera2-pipe-testing/src/robolectric/java/androidx/camera/camera2/pipe/testing/RobolectricCameras.kt
rename to camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameras.kt
index 004cdf4..d21494a 100644
--- a/camera/camera-camera2-pipe-testing/src/robolectric/java/androidx/camera/camera2/pipe/testing/RobolectricCameras.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameras.kt
@@ -25,14 +25,20 @@
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
+import android.os.Build
import android.os.Handler
import android.os.Looper
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.CameraMetadata
import androidx.camera.camera2.pipe.compat.Camera2CameraMetadata
import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth
import kotlinx.atomicfu.atomic
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
import org.robolectric.Shadows.shadowOf
+import org.robolectric.annotation.Config
import org.robolectric.shadow.api.Shadow
import org.robolectric.shadows.ShadowApplication
import org.robolectric.shadows.ShadowCameraCharacteristics
@@ -169,3 +175,32 @@
}
}
}
+
+@RunWith(RobolectricCameraPipeTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class RobolectricCamerasTest {
+ private val context = ApplicationProvider.getApplicationContext() as Context
+ private val mainLooper = shadowOf(Looper.getMainLooper())
+
+ @Test
+ fun fakeCamerasCanBeOpened() {
+ val fakeCameraId = RobolectricCameras.create(
+ mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
+ )
+ val fakeCamera = RobolectricCameras.open(fakeCameraId)
+
+ Truth.assertThat(fakeCamera).isNotNull()
+ Truth.assertThat(fakeCamera.cameraId).isEqualTo(fakeCameraId)
+ Truth.assertThat(fakeCamera.cameraDevice).isNotNull()
+ Truth.assertThat(fakeCamera.characteristics).isNotNull()
+ Truth.assertThat(fakeCamera.characteristics[CameraCharacteristics.LENS_FACING]).isNotNull()
+ Truth.assertThat(fakeCamera.metadata).isNotNull()
+ Truth.assertThat(fakeCamera.metadata[CameraCharacteristics.LENS_FACING]).isNotNull()
+ }
+
+ @After
+ fun teardown() {
+ mainLooper.idle()
+ RobolectricCameras.clear()
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTest.kt
index 6d1c986..71554f9 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureTest.kt
@@ -32,6 +32,7 @@
import androidx.camera.testing.CameraUtil
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
@@ -173,6 +174,7 @@
parcelFileDescriptor.close()
}
+ @FlakyTest // b/182165222
@Test
fun unbind_shouldStopRecording() {
val file = File.createTempFile("CameraX", ".tmp").apply {
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/ConstantObservableTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/ConstantObservableTest.kt
index 8e6a633..a76dc4d 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/ConstantObservableTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/ConstantObservableTest.kt
@@ -16,17 +16,20 @@
package androidx.camera.core.impl
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.testing.asFlow
import androidx.concurrent.futures.await
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
private const val MAGIC_VALUE = 42
+private const val MAGIC_STRING = "Magic"
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -52,4 +55,27 @@
val value = constantObservable.asFlow().first()
assertThat(value).isEqualTo(MAGIC_VALUE)
}
+
+ @Test
+ public fun observerOfSuperClass_receivesValue(): Unit = runBlocking {
+ val constantObservable: Observable<String> = ConstantObservable.withValue(MAGIC_STRING)
+
+ // Create on observer of CharSequence, a superclass of String
+ val deferredValue = CompletableDeferred<CharSequence?>()
+ val observer: Observable.Observer<CharSequence> = object :
+ Observable.Observer<CharSequence> {
+ override fun onNewData(value: CharSequence?) {
+ deferredValue.complete(value)
+ }
+
+ override fun onError(t: Throwable) {
+ deferredValue.completeExceptionally(t)
+ }
+ }
+
+ // Add the observer to receive the value
+ constantObservable.addObserver(CameraXExecutors.directExecutor(), observer)
+
+ assertThat(deferredValue.await()).isEqualTo(MAGIC_STRING)
+ }
}
\ No newline at end of file
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/LiveDataObservableTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/LiveDataObservableTest.kt
index a4f3b38..805fb7b 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/LiveDataObservableTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/LiveDataObservableTest.kt
@@ -115,4 +115,4 @@
}
}
-private class TestError(message: String) : Exception(message)
\ No newline at end of file
+internal class TestError(message: String) : Exception(message)
\ No newline at end of file
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/StateObservableTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/StateObservableTest.kt
new file mode 100644
index 0000000..0d901b2
--- /dev/null
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/impl/StateObservableTest.kt
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl
+
+import androidx.camera.testing.asFlow
+import androidx.concurrent.futures.await
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SmallTest
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.dropWhile
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val INITIAL_STATE = 0
+private const val MAGIC_STATE = 42
+private val TEST_ERROR = TestError("TEST")
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+public class StateObservableTest {
+
+ @Test
+ public fun canFetchInitialState(): Unit = runBlocking {
+ val observable = MutableStateObservable.withInitialState(INITIAL_STATE)
+
+ val fetched = observable.fetchData().await()
+ assertThat(fetched).isEqualTo(INITIAL_STATE)
+ }
+
+ @Test
+ public fun canFetchInitialError(): Unit = runBlocking {
+ val observable = MutableStateObservable.withInitialError<Any?>(TEST_ERROR)
+
+ assertThrows<TestError> { observable.fetchData().await() }
+ }
+
+ @Test
+ public fun fetchIsImmediate() {
+ val observable = MutableStateObservable.withInitialState(INITIAL_STATE)
+
+ assertThat(observable.fetchData().isDone).isTrue()
+ }
+
+ @Test
+ public fun canObserveToFetchInitialState(): Unit = runBlocking {
+ val observable = MutableStateObservable.withInitialState(INITIAL_STATE)
+
+ val fetched = observable.asFlow().first()
+ assertThat(fetched).isEqualTo(INITIAL_STATE)
+ }
+
+ @Test
+ public fun canObserveToFetchInitialError(): Unit = runBlocking {
+ val observable = MutableStateObservable.withInitialError<Any?>(TEST_ERROR)
+
+ assertThrows<TestError> { observable.asFlow().first() }
+ }
+
+ @Test
+ public fun canObserveToFetchPostedState(): Unit = runBlocking {
+ val observable = MutableStateObservable.withInitialState(INITIAL_STATE)
+ observable.setState(MAGIC_STATE)
+
+ val fetched = observable.asFlow().dropWhile {
+ it != MAGIC_STATE
+ }.first()
+ assertThat(fetched).isEqualTo(MAGIC_STATE)
+ }
+
+ @Test
+ public fun canObserveToReceivePostedError(): Unit = runBlocking {
+ val observable = MutableStateObservable.withInitialState(INITIAL_STATE)
+ observable.setError(TEST_ERROR)
+
+ assertThrows<TestError> { observable.asFlow().collect() }
+ }
+
+ @Test
+ public fun canSetAndFetchState_fromSeparateThreads(): Unit = runBlocking {
+ val observable = MutableStateObservable.withInitialState(INITIAL_STATE)
+ launch(Dispatchers.Main) {
+ delay(100) // Not strictly necessary, but wait before setting state
+ observable.setState(MAGIC_STATE)
+ }
+
+ val fetched = async(Dispatchers.IO) {
+ observable.asFlow().dropWhile { it != MAGIC_STATE }.first()
+ }
+
+ assertThat(fetched.await()).isEqualTo(MAGIC_STATE)
+ }
+
+ @Test
+ public fun canObserveToRetrieveState_whenSetAfterObserve(): Unit = runBlocking {
+ val observable = MutableStateObservable.withInitialState(INITIAL_STATE)
+
+ // Add the observer
+ val deferred = CompletableDeferred<Int>()
+ observable.addObserver(
+ Dispatchers.IO.asExecutor(),
+ object : Observable.Observer<Int> {
+ override fun onNewData(state: Int?) {
+ if (state == MAGIC_STATE) {
+ deferred.complete(state)
+ }
+ }
+
+ override fun onError(t: Throwable) {
+ deferred.completeExceptionally(t)
+ }
+ }
+ )
+
+ // Post the state
+ observable.setState(MAGIC_STATE)
+
+ assertThat(deferred.await()).isEqualTo(MAGIC_STATE)
+ }
+
+ @Test
+ public fun canObserveToRetrieveError_whenSetAfterObserve(): Unit = runBlocking {
+ val observable = MutableStateObservable.withInitialState(INITIAL_STATE)
+
+ // Add the observer
+ val deferred = CompletableDeferred<Int>()
+ observable.addObserver(
+ Dispatchers.IO.asExecutor(),
+ object : Observable.Observer<Int> {
+ override fun onNewData(state: Int?) {
+ // Ignore states
+ }
+
+ override fun onError(t: Throwable) {
+ deferred.completeExceptionally(t)
+ }
+ }
+ )
+
+ // Post the error
+ observable.setError(TEST_ERROR)
+
+ assertThrows<TestError> { deferred.await() }
+ }
+
+ @MediumTest
+ @Test
+ public fun allObservers_receiveFinalState(): Unit = runBlocking {
+ val observable = MutableStateObservable.withInitialState(INITIAL_STATE)
+
+ // Create 20 observers to ensure they all complete
+ val receiveJob = launch(Dispatchers.IO) {
+ repeat(20) {
+ launch { observable.asFlow().dropWhile { it != MAGIC_STATE }.first() }
+ }
+ }
+
+ // Create another coroutine to set states
+ launch(Dispatchers.IO) {
+ (25 downTo 0).forEach { i ->
+ observable.setState(MAGIC_STATE - i)
+ delay(5)
+ }
+ }
+
+ // Ensure receiveJob completes
+ receiveJob.join()
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
index 59afcfc..4ae94f0 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
@@ -22,7 +22,6 @@
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.annotation.StringDef;
-import androidx.camera.core.impl.CamcorderProfileProvider;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
@@ -164,13 +163,4 @@
@RestrictTo(Scope.LIBRARY_GROUP)
@interface ImplementationType {
}
-
- /**
- * Returns the {@link CamcorderProfileProvider} associated with this camera.
- *
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- CamcorderProfileProvider getCamcorderProfileProvider();
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
index a5f6579..9e9ee15 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
@@ -822,6 +822,7 @@
// Audio encoding loop. Exits on end of stream.
boolean audioEos = false;
int outIndex;
+ long lastAudioTimestamp = 0;
while (!audioEos && mIsRecording) {
// Check for end of stream from main thread
if (mEndOfAudioStreamSignal.get()) {
@@ -862,7 +863,20 @@
case MediaCodec.INFO_TRY_AGAIN_LATER:
break;
default:
- audioEos = writeAudioEncodedBuffer(outIndex);
+ // Drops out of order audio frame if the frame's earlier than last
+ // frame.
+ if (mAudioBufferInfo.presentationTimeUs > lastAudioTimestamp) {
+ audioEos = writeAudioEncodedBuffer(outIndex);
+ lastAudioTimestamp = mAudioBufferInfo.presentationTimeUs;
+ } else {
+ Logger.w(TAG,
+ "Drops frame, current frame's timestamp "
+ + mAudioBufferInfo.presentationTimeUs
+ + " is earlier that last frame "
+ + lastAudioTimestamp);
+ // Releases this frame from output buffer
+ mAudioEncoder.releaseOutputBuffer(outIndex, false);
+ }
}
} while (outIndex >= 0 && !audioEos); // end of dequeue output buffer
}
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 7dc5669..aba76ae 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
@@ -66,4 +66,8 @@
/** Returns a list of quirks related to the camera. */
@NonNull
Quirks getCameraQuirks();
+
+ /** Returns the {@link CamcorderProfileProvider} associated with this camera. */
+ @NonNull
+ CamcorderProfileProvider getCamcorderProfileProvider();
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ConstantObservable.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ConstantObservable.java
index 17fddbb..f090d00c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ConstantObservable.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ConstantObservable.java
@@ -62,7 +62,7 @@
}
@Override
- public void addObserver(@NonNull Executor executor, @NonNull Observable.Observer<T> observer) {
+ public void addObserver(@NonNull Executor executor, @NonNull Observer<? super T> observer) {
// Since the Observable has a constant value, we only will have a one-shot call to the
// observer, so we don't need to store the observer.
// ImmediateFuture does not actually store listeners since it is already complete, so it
@@ -79,7 +79,7 @@
}
@Override
- public void removeObserver(@NonNull Observable.Observer<T> observer) {
+ public void removeObserver(@NonNull Observer<? super T> observer) {
// no-op. addObserver() does not need to store observers.
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/LiveDataObservable.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/LiveDataObservable.java
index fba389e..e8520da 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/LiveDataObservable.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/LiveDataObservable.java
@@ -53,7 +53,7 @@
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final MutableLiveData<Result<T>> mLiveData = new MutableLiveData<>();
@GuardedBy("mObservers")
- private final Map<Observer<T>, LiveDataObserverAdapter<T>> mObservers = new HashMap<>();
+ private final Map<Observer<? super T>, LiveDataObserverAdapter<T>> mObservers = new HashMap<>();
/**
* Posts a new value to be used as the current value of this Observable.
@@ -81,34 +81,26 @@
@Override
@SuppressWarnings("ObjectToString")
public ListenableFuture<T> fetchData() {
- return CallbackToFutureAdapter.getFuture(new CallbackToFutureAdapter.Resolver<T>() {
- @Nullable
- @Override
- public Object attachCompleter(
- @NonNull final CallbackToFutureAdapter.Completer<T> completer) {
- CameraXExecutors.mainThreadExecutor().execute(new Runnable() {
- @Override
- public void run() {
- Result<T> result = mLiveData.getValue();
- if (result == null) {
- completer.setException(new IllegalStateException(
- "Observable has not yet been initialized with a value."));
- } else if (result.completedSuccessfully()) {
- completer.set(result.getValue());
- } else {
- Preconditions.checkNotNull(result.getError());
- completer.setException(result.getError());
- }
- }
- });
+ return CallbackToFutureAdapter.getFuture(completer -> {
+ CameraXExecutors.mainThreadExecutor().execute(() -> {
+ Result<T> result = mLiveData.getValue();
+ if (result == null) {
+ completer.setException(new IllegalStateException(
+ "Observable has not yet been initialized with a value."));
+ } else if (result.completedSuccessfully()) {
+ completer.set(result.getValue());
+ } else {
+ Preconditions.checkNotNull(result.getError());
+ completer.setException(result.getError());
+ }
+ });
- return LiveDataObservable.this + " [fetch@" + SystemClock.uptimeMillis() + "]";
- }
+ return LiveDataObservable.this + " [fetch@" + SystemClock.uptimeMillis() + "]";
});
}
@Override
- public void addObserver(@NonNull Executor executor, @NonNull Observer<T> observer) {
+ public void addObserver(@NonNull Executor executor, @NonNull Observer<? super T> observer) {
synchronized (mObservers) {
final LiveDataObserverAdapter<T> oldAdapter = mObservers.get(observer);
if (oldAdapter != null) {
@@ -119,29 +111,24 @@
observer);
mObservers.put(observer, newAdapter);
- CameraXExecutors.mainThreadExecutor().execute(new Runnable() {
- @Override
- public void run() {
+ CameraXExecutors.mainThreadExecutor().execute(() -> {
+ if (oldAdapter != null) {
mLiveData.removeObserver(oldAdapter);
- mLiveData.observeForever(newAdapter);
}
+ mLiveData.observeForever(newAdapter);
});
}
}
@Override
- public void removeObserver(@NonNull Observer<T> observer) {
+ public void removeObserver(@NonNull Observer<? super T> observer) {
synchronized (mObservers) {
LiveDataObserverAdapter<T> adapter = mObservers.remove(observer);
if (adapter != null) {
adapter.disable();
- CameraXExecutors.mainThreadExecutor().execute(new Runnable() {
- @Override
- public void run() {
- mLiveData.removeObserver(adapter);
- }
- });
+ CameraXExecutors.mainThreadExecutor().execute(
+ () -> mLiveData.removeObserver(adapter));
}
}
}
@@ -156,9 +143,9 @@
*/
public static final class Result<T> {
@Nullable
- private T mValue;
+ private final T mValue;
@Nullable
- private Throwable mError;
+ private final Throwable mError;
private Result(@Nullable T value, @Nullable Throwable error) {
mValue = value;
@@ -224,10 +211,10 @@
androidx.lifecycle.Observer<Result<T>> {
final AtomicBoolean mActive = new AtomicBoolean(true);
- final Observer<T> mObserver;
+ final Observer<? super T> mObserver;
final Executor mExecutor;
- LiveDataObserverAdapter(@NonNull Executor executor, @NonNull Observer<T> observer) {
+ LiveDataObserverAdapter(@NonNull Executor executor, @NonNull Observer<? super T> observer) {
mExecutor = executor;
mObserver = observer;
}
@@ -238,20 +225,17 @@
@Override
public void onChanged(@NonNull final Result<T> result) {
- mExecutor.execute(new Runnable() {
- @Override
- public void run() {
- if (!mActive.get()) {
- // Observer has been disabled.
- return;
- }
+ mExecutor.execute(() -> {
+ if (!mActive.get()) {
+ // Observer has been disabled.
+ return;
+ }
- if (result.completedSuccessfully()) {
- mObserver.onNewData(result.getValue());
- } else {
- Preconditions.checkNotNull(result.getError());
- mObserver.onError(result.getError());
- }
+ if (result.completedSuccessfully()) {
+ mObserver.onNewData(result.getValue());
+ } else {
+ Preconditions.checkNotNull(result.getError());
+ mObserver.onError(result.getError());
}
});
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/MutableStateObservable.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/MutableStateObservable.java
new file mode 100644
index 0000000..90fa540
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/MutableStateObservable.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * A {@link StateObservable} whose state can be set.
+ *
+ * @param <T> The state type.
+ */
+public class MutableStateObservable<T> extends StateObservable<T> {
+
+ private MutableStateObservable(@Nullable Object initialState, boolean isError) {
+ super(initialState, isError);
+ }
+
+ /**
+ * Creates a mutable state observer with the provided initial state.
+ *
+ * @param initialState The initial state
+ * @param <T> The state type
+ * @return A mutable state observable initialized with the given initial state.
+ */
+ @NonNull
+ public static <T> MutableStateObservable<T> withInitialState(@Nullable T initialState) {
+ return new MutableStateObservable<>(initialState, false);
+ }
+
+ /**
+ * Creates a mutable state observer in an initial error state containing the provided
+ * {@link Throwable}.
+ *
+ * @param initialError The {@link Throwable} contained by the error state.
+ * @param <T> The state type
+ * @return A mutable state observable initialized in an error state containing the provided
+ * {@link Throwable}.
+ */
+ @NonNull
+ public static <T> MutableStateObservable<T> withInitialError(@NonNull Throwable initialError) {
+ return new MutableStateObservable<>(initialError, true);
+ }
+
+ /**
+ * Posts a new state to be used as the current state of this Observable.
+ */
+ public void setState(@Nullable T state) {
+ updateState(state);
+ }
+
+ /**
+ * Posts a new {@link Throwable} to be used in the new error state of this Observable.
+ */
+ public void setError(@NonNull Throwable error) {
+ updateStateAsError(error);
+ }
+
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/Observable.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/Observable.java
index 272e1cc..4491d48 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/Observable.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/Observable.java
@@ -55,11 +55,10 @@
*
* <p>If the same observer is added twice, it will only be called on the last executor it was
* registered with.
- *
* @param executor The executor which will be used to notify the observer of new data.
* @param observer The observer which will receive new data.
*/
- void addObserver(@NonNull Executor executor, @NonNull Observer<T> observer);
+ void addObserver(@NonNull Executor executor, @NonNull Observer<? super T> observer);
/**
* Removes a previously added observer.
@@ -70,7 +69,7 @@
*
* @param observer The observer to remove.
*/
- void removeObserver(@NonNull Observer<T> observer);
+ void removeObserver(@NonNull Observer<? super T> observer);
/**
* A callback that can receive new values and errors from an {@link Observable}.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/StateObservable.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/StateObservable.java
new file mode 100644
index 0000000..ed3ab43
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/StateObservable.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.impl;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.core.util.Preconditions;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * An observable which reports a dynamic state.
+ *
+ * <p>The state of a state observable is conflated. That is, the value received by an
+ * {@link androidx.camera.core.impl.Observable.Observer} will only be the latest state; some
+ * state updates may never be observed if the state changes quickly enough.
+ *
+ * <p>State observables require an initial state, and thus always have a state available for
+ * retrieval via {@link #fetchData()}, which will return an already-complete
+ * {@link ListenableFuture}.
+ *
+ * <p>Errors are also possible as states, and when an error is present, any previous state
+ * information is lost. State observables may transition in and out of error states at any time,
+ * including the initial state.
+ *
+ * <p>All states, including errors, are conflated via {@link Object#equals(Object)}. That is, if
+ * two states evaluate to {@code true}, it will be as if the state didn't change and no update
+ * will be sent to observers.
+ *
+ * @param <T> The state type.
+ */
+public abstract class StateObservable<T> implements Observable<T> {
+ private static final int INITIAL_VERSION = 0;
+
+ private final Object mLock = new Object();
+ private final AtomicReference<Object> mState;
+ @GuardedBy("mLock")
+ private int mVersion = INITIAL_VERSION;
+ @GuardedBy("mLock")
+ private boolean mUpdating = false;
+
+ // Must be updated together under lock
+ @GuardedBy("mLock")
+ private final Map<Observer<? super T>, ObserverWrapper<T>> mWrapperMap = new HashMap<>();
+ @GuardedBy("mLock")
+ private final CopyOnWriteArraySet<ObserverWrapper<T>> mNotifySet = new CopyOnWriteArraySet<>();
+
+ StateObservable(@Nullable Object initialState, boolean isError) {
+ if (isError) {
+ Preconditions.checkArgument(initialState instanceof Throwable, "Initial errors must "
+ + "be Throwable");
+ mState = new AtomicReference<>(ErrorWrapper.wrap((Throwable) initialState));
+ } else {
+ mState = new AtomicReference<>(initialState);
+ }
+
+ }
+
+ void updateState(@Nullable T state) {
+ updateStateInternal(state);
+ }
+
+ void updateStateAsError(@NonNull Throwable error) {
+ updateStateInternal(ErrorWrapper.wrap(error));
+ }
+
+ private void updateStateInternal(@Nullable Object newState) {
+ Iterator<ObserverWrapper<T>> notifyIter;
+ int currentVersion;
+ synchronized (mLock) {
+ Object oldState = mState.getAndSet(newState);
+ // If new state is equal to old state, no need to do anything.
+ if (Objects.equals(oldState, newState)) return;
+ currentVersion = ++mVersion; // State was updated. Next version.
+ if (mUpdating) return; // Already updating. New state will get used due to version bump.
+ mUpdating = true;
+ notifyIter = mNotifySet.iterator();
+ }
+
+ while (true) {
+ // Update observers unlocked in case of direct executor.
+ while (notifyIter.hasNext()) {
+ notifyIter.next().update(currentVersion);
+ }
+
+ // Check if a new version was added while updating
+ synchronized (mLock) {
+ if (mVersion == currentVersion) {
+ // Updating complete. Break out.
+ mUpdating = false;
+ break;
+ }
+
+ // A new version was added. Update again on next loop.
+ // Get a new iterator in case the observers changed during update.
+ notifyIter = mNotifySet.iterator();
+ currentVersion = mVersion;
+ }
+ }
+ }
+
+ /**
+ * Fetch the latest state.
+ *
+ * <p>For state observables, the future returned by {@code fetchData()} is guaranteed to be
+ * complete and will contain either the current state or an error state which will be thrown
+ * as an exception from {@link ListenableFuture#get()}.
+ *
+ * @return A future which will contain the latest value or an error.
+ */
+ @SuppressWarnings("unchecked")
+ @NonNull
+ @Override
+ public ListenableFuture<T> fetchData() {
+ Object state = mState.get();
+ if (state instanceof ErrorWrapper) {
+ return Futures.immediateFailedFuture(((ErrorWrapper) state).getError());
+ } else {
+ return Futures.immediateFuture((T) state);
+ }
+ }
+
+ @Override
+ public void addObserver(@NonNull Executor executor, @NonNull Observer<? super T> observer) {
+ ObserverWrapper<T> wrapper;
+ synchronized (mLock) {
+ // If observer is already registered, remove it. It will get notified again immediately.
+ removeObserverLocked(observer);
+
+ wrapper = new ObserverWrapper<>(mState, executor, observer);
+ mWrapperMap.put(observer, wrapper);
+ mNotifySet.add(wrapper);
+ }
+
+ // INITIAL_VERSION won't necessarily match the current tracked version constant, but it
+ // will be the initial version this wrapper receives. Any future version updates will
+ // always be higher than INITIAL_VERSION.
+ wrapper.update(INITIAL_VERSION);
+ }
+
+ @Override
+ public void removeObserver(@NonNull Observer<? super T> observer) {
+ synchronized (mLock) {
+ removeObserverLocked(observer);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void removeObserverLocked(@NonNull Observable.Observer<? super T> observer) {
+ ObserverWrapper<T> wrapper = mWrapperMap.remove(observer);
+ if (wrapper != null) {
+ wrapper.close();
+ mNotifySet.remove(wrapper);
+ }
+ }
+
+ private static final class ObserverWrapper<T> implements Runnable {
+ private static final Object NOT_SET = new Object();
+ private static final int NO_VERSION = -1;
+
+ private final Executor mExecutor;
+ private final Observer<? super T> mObserver;
+ private final AtomicBoolean mActive = new AtomicBoolean(true);
+ private final AtomicReference<Object> mStateRef;
+
+ // Since run() will always run sequentially, no need to lock for this variable.
+ private Object mLastState = NOT_SET;
+ @GuardedBy("this")
+ private int mLatestSignalledVersion = NO_VERSION;
+ @GuardedBy("this")
+ private boolean mWrapperUpdating = false;
+
+ ObserverWrapper(@NonNull AtomicReference<Object> stateRef, @NonNull Executor executor,
+ @NonNull Observer<? super T> observer) {
+ mStateRef = stateRef;
+ mExecutor = executor;
+ mObserver = observer;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void run() {
+ Object newState;
+ int currentVersion;
+ synchronized (this) {
+ // Only update if we're still active.
+ if (!mActive.get()) {
+ mWrapperUpdating = false;
+ return;
+ }
+ // Get latest state.
+ newState = mStateRef.get();
+ currentVersion = mLatestSignalledVersion;
+ }
+
+ // Continue to check if we're active before updating
+ while (true) {
+ // Conflate notification using equality
+ if (!Objects.equals(mLastState, newState)) {
+ mLastState = newState;
+ if (newState instanceof ErrorWrapper) {
+ mObserver.onError(((ErrorWrapper) newState).getError());
+ } else {
+ mObserver.onNewData((T) newState);
+ }
+ }
+
+ synchronized (this) {
+ if (currentVersion == mLatestSignalledVersion || !mActive.get()) {
+ // Updating complete or no longer active. Break out of update loop.
+ mWrapperUpdating = false;
+ break;
+ }
+
+ // Get state and version for next update.
+ newState = mStateRef.get();
+ currentVersion = mLatestSignalledVersion;
+ }
+ }
+ }
+
+ void update(int version) {
+ synchronized (this) {
+ // If no longer active, then don't attempt update.
+ if (!mActive.get()) return;
+ // No need to update (but this probably shouldn't happen anyways)
+ if (version <= mLatestSignalledVersion) return;
+ mLatestSignalledVersion = version;
+ // No need to update if already updating. Version bump will cause update.
+ if (mWrapperUpdating) return;
+ mWrapperUpdating = true;
+ }
+
+ try {
+ mExecutor.execute(this);
+ } catch (Throwable t) {
+ // Unable to notify due to state of Executor. The update is lost, but there's
+ // not much we can do here since the executor rejected the update. Note this
+ // may also mean that any updates which occurred while mWrapperUpdating ==
+ // true will have also been lost.
+ synchronized (this) {
+ // Update mWrapperUpdating so the next update can try again
+ mWrapperUpdating = false;
+ }
+ }
+ }
+
+ void close() {
+ // Best effort cancellation. In progress updates will not be cancelled.
+ mActive.set(false);
+ }
+ }
+
+ @AutoValue
+ abstract static class ErrorWrapper {
+ @NonNull
+ static ErrorWrapper wrap(@NonNull Throwable error) {
+ return new AutoValue_StateObservable_ErrorWrapper(error);
+ }
+
+ @NonNull
+ public abstract Throwable getError();
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
index 35c85a4..0a15ab7 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
@@ -47,10 +47,6 @@
quirks.add(new ImageCaptureRotationOptionQuirk());
}
- if (PreviewStretchedQuirk.load()) {
- quirks.add(new PreviewStretchedQuirk());
- }
-
return quirks;
}
}
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 134a045..7be0d3a 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
@@ -58,7 +58,11 @@
private final MutableLiveData<ZoomState> mZoomLiveData;
private String mImplementationType = IMPLEMENTATION_TYPE_FAKE;
- private CamcorderProfileProvider mCamcorderProfileProvider = CamcorderProfileProvider.EMPTY;
+
+ // Leave uninitialized to support camera-core:1.0.0 dependencies.
+ // Can be initialized during class init once there are no more pinned dependencies on
+ // camera-core:1.0.0
+ private CamcorderProfileProvider mCamcorderProfileProvider;
@NonNull
private final List<Quirk> mCameraQuirks = new ArrayList<>();
@@ -169,7 +173,8 @@
@NonNull
@Override
public CamcorderProfileProvider getCamcorderProfileProvider() {
- return mCamcorderProfileProvider;
+ return mCamcorderProfileProvider == null ? CamcorderProfileProvider.EMPTY :
+ mCamcorderProfileProvider;
}
@Override
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
index db8e4be..f2d0dd6 100644
--- 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
@@ -29,7 +29,7 @@
private val lock = Object()
@GuardedBy("lock")
- private val observers = mutableMapOf<Observable.Observer<BufferProvider.State>, Executor>()
+ private val observers = mutableMapOf<Observable.Observer<in BufferProvider.State>, Executor>()
@GuardedBy("lock")
private var state = BufferProvider.State.ACTIVE
@@ -51,7 +51,7 @@
override fun addObserver(
executor: Executor,
- observer: Observable.Observer<BufferProvider.State>
+ observer: Observable.Observer<in BufferProvider.State>
) {
synchronized(observers) {
observers[observer] = executor
@@ -59,7 +59,7 @@
executor.execute { observer.onNewData(state) }
}
- override fun removeObserver(observer: Observable.Observer<BufferProvider.State>) {
+ override fun removeObserver(observer: Observable.Observer<in BufferProvider.State>) {
synchronized(lock) {
observers.remove(observer)
}
@@ -67,7 +67,7 @@
fun setActive(active: Boolean) {
val newState = if (active) BufferProvider.State.ACTIVE else BufferProvider.State.INACTIVE
- val localObservers: Map<Observable.Observer<BufferProvider.State>, Executor>
+ val localObservers: Map<Observable.Observer<in BufferProvider.State>, Executor>
synchronized(lock) {
if (state == newState) {
return
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java
index 21d82f6..01096ab 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java
@@ -827,6 +827,7 @@
// Audio encoding loop. Exits on end of stream.
boolean audioEos = false;
int outIndex;
+ long lastAudioTimestamp = 0;
while (!audioEos && mIsRecording) {
// Check for end of stream from main thread
if (mEndOfAudioStreamSignal.get()) {
@@ -867,7 +868,20 @@
case MediaCodec.INFO_TRY_AGAIN_LATER:
break;
default:
- audioEos = writeAudioEncodedBuffer(outIndex);
+ // Drops out of order audio frame if the frame's earlier than last
+ // frame.
+ if (mAudioBufferInfo.presentationTimeUs >= lastAudioTimestamp) {
+ audioEos = writeAudioEncodedBuffer(outIndex);
+ lastAudioTimestamp = mAudioBufferInfo.presentationTimeUs;
+ } else {
+ Logger.w(TAG,
+ "Drops frame, current frame's timestamp "
+ + mAudioBufferInfo.presentationTimeUs
+ + " is earlier that last frame "
+ + lastAudioTimestamp);
+ // Releases this frame from output buffer
+ mAudioEncoder.releaseOutputBuffer(outIndex, false);
+ }
}
} while (outIndex >= 0 && !audioEos); // end of dequeue output buffer
}
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 132003d..3f6e043 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
@@ -786,12 +786,14 @@
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
class InternalStateObservable implements Observable<InternalState> {
- private final Map<Observer<InternalState>, Executor> mObservers = new LinkedHashMap<>();
+ private final Map<Observer<? super InternalState>, Executor> mObservers =
+ new LinkedHashMap<>();
@ExecutedBy("mEncoderExecutor")
void notifyState() {
final InternalState state = mState;
- for (Map.Entry<Observer<InternalState>, Executor> entry : mObservers.entrySet()) {
+ for (Map.Entry<Observer<? super InternalState>, Executor> entry :
+ mObservers.entrySet()) {
entry.getValue().execute(() -> entry.getKey().onNewData(state));
}
}
@@ -806,7 +808,7 @@
@ExecutedBy("mEncoderExecutor")
@Override
public void addObserver(@NonNull Executor executor,
- @NonNull Observer<InternalState> observer) {
+ @NonNull Observer<? super InternalState> observer) {
final InternalState state = mState;
mObservers.put(observer, executor);
executor.execute(() -> observer.onNewData(state));
@@ -814,7 +816,7 @@
@ExecutedBy("mEncoderExecutor")
@Override
- public void removeObserver(@NonNull Observer<InternalState> observer) {
+ public void removeObserver(@NonNull Observer<? super InternalState> observer) {
mObservers.remove(observer);
}
}
@@ -1114,7 +1116,8 @@
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
class ByteBufferInput implements Encoder.ByteBufferInput {
- private final Map<Observer<State>, Executor> mStateObservers = new LinkedHashMap<>();
+ private final Map<Observer<? super State>, Executor> mStateObservers =
+ new LinkedHashMap<>();
private State mBufferProviderState = State.INACTIVE;
@@ -1163,7 +1166,8 @@
/** {@inheritDoc} */
@Override
- public void addObserver(@NonNull Executor executor, @NonNull Observer<State> observer) {
+ public void addObserver(@NonNull Executor executor,
+ @NonNull Observer<? super State> observer) {
mEncoderExecutor.execute(() -> {
mStateObservers.put(Preconditions.checkNotNull(observer),
Preconditions.checkNotNull(executor));
@@ -1174,7 +1178,7 @@
/** {@inheritDoc} */
@Override
- public void removeObserver(@NonNull Observer<State> observer) {
+ public void removeObserver(@NonNull Observer<? super State> observer) {
mEncoderExecutor.execute(
() -> mStateObservers.remove(Preconditions.checkNotNull(observer)));
}
@@ -1194,7 +1198,7 @@
mAcquisitionList.clear();
}
- for (Map.Entry<Observer<State>, Executor> entry : mStateObservers.entrySet()) {
+ for (Map.Entry<Observer<? super State>, Executor> entry : mStateObservers.entrySet()) {
try {
entry.getValue().execute(() -> entry.getKey().onNewData(newState));
} catch (RejectedExecutionException e) {
diff --git a/camera/camera-view/build.gradle b/camera/camera-view/build.gradle
index edb66a5..1ad6270 100644
--- a/camera/camera-view/build.gradle
+++ b/camera/camera-view/build.gradle
@@ -25,11 +25,13 @@
id("kotlin-android")
}
+apply(from: "dependencies.gradle")
+
dependencies {
implementation("androidx.appcompat:appcompat:1.1.0")
api("androidx.lifecycle:lifecycle-common:2.0.0")
- api(project(":camera:camera-core"))
- implementation(project(path: ":camera:camera-lifecycle"))
+ api("androidx.camera:camera-core:${VIEW_ATOMIC_GROUP_PINNED_VER}")
+ implementation("androidx.camera:camera-lifecycle:${VIEW_ATOMIC_GROUP_PINNED_VER}")
api("androidx.annotation:annotation:1.0.0")
implementation(GUAVA_LISTENABLE_FUTURE)
implementation("androidx.core:core:1.1.0")
@@ -44,9 +46,17 @@
testImplementation(TRUTH)
testImplementation(ANDROIDX_TEST_RULES)
testImplementation(ANDROIDX_TEST_CORE)
- testImplementation(project(":camera:camera-testing"))
+ testImplementation(project(":camera:camera-testing")) {
+ // Ensure camera-testing does not pull in camera-core project dependency which will
+ // override pinned dependency.
+ exclude(group:"androidx.camera", module:"camera-core")
+ }
- androidTestImplementation(project(":camera:camera-testing"))
+ androidTestImplementation(project(":camera:camera-testing")) {
+ // Ensure camera-testing does not pull in camera-core project dependency which will
+ // override pinned dependency.
+ exclude(group:"androidx.camera", module:"camera-core")
+ }
androidTestImplementation(MOCKITO_CORE)
androidTestImplementation(ESPRESSO_CORE)
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
@@ -56,7 +66,7 @@
androidTestImplementation(ANDROIDX_TEST_UIAUTOMATOR)
androidTestImplementation(KOTLIN_STDLIB)
androidTestImplementation(TRUTH)
- androidTestImplementation(project(":camera:camera-camera2"))
+ androidTestImplementation("androidx.camera:camera-camera2:${VIEW_ATOMIC_GROUP_PINNED_VER}")
androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it's own MockMaker
androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it's own MockMaker
}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt b/camera/camera-view/dependencies.gradle
similarity index 78%
copy from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
copy to camera/camera-view/dependencies.gradle
index 7e01354..5795377 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
+++ b/camera/camera-view/dependencies.gradle
@@ -1,5 +1,3 @@
-// ktlint-disable filename
-
/*
* Copyright 2021 The Android Open Source Project
*
@@ -16,6 +14,7 @@
* limitations under the License.
*/
-package androidx.compose.foundation
-
-internal actual typealias AtomicReference<V> = java.util.concurrent.atomic.AtomicReference<V>
\ No newline at end of file
+ext {
+ // camera-view temporarily pins same-group depenencies to RC/stable until beta
+ VIEW_ATOMIC_GROUP_PINNED_VER = "1.0.0-rc03"
+}
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewTest.java b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewTest.java
index 41a8c1c..509895a 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewTest.java
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewTest.java
@@ -173,6 +173,45 @@
}
@Test
+ public void receiveSurfaceRequest_transformIsValid() throws InterruptedException {
+ // Arrange: set up PreviewView.
+ AtomicReference<PreviewView> previewView = new AtomicReference<>();
+ CountDownLatch countDownLatch = new CountDownLatch(1);
+ mInstrumentation.runOnMainSync(() -> {
+ previewView.set(new PreviewView(mContext));
+ setContentView(previewView.get());
+ // Feed the PreviewView with a fake SurfaceRequest
+ CameraInfo cameraInfo = createCameraInfo(CameraInfo.IMPLEMENTATION_TYPE_CAMERA2);
+ previewView.get().getSurfaceProvider().onSurfaceRequested(
+ createSurfaceRequest(cameraInfo));
+ notifyLatchWhenLayoutReady(previewView.get(), countDownLatch);
+ });
+ updateCropRectAndWaitForIdle(DEFAULT_CROP_RECT);
+
+ // Assert: OutputTransform is not null.
+ assertThat(countDownLatch.await(1, TimeUnit.SECONDS)).isTrue();
+ mInstrumentation.runOnMainSync(
+ () -> assertThat(previewView.get().getOutputTransform()).isNotNull());
+ }
+
+ @Test
+ public void noSurfaceRequest_transformIsInvalid() throws InterruptedException {
+ // Arrange: set up PreviewView.
+ AtomicReference<PreviewView> previewView = new AtomicReference<>();
+ CountDownLatch countDownLatch = new CountDownLatch(1);
+ mInstrumentation.runOnMainSync(() -> {
+ previewView.set(new PreviewView(mContext));
+ setContentView(previewView.get());
+ notifyLatchWhenLayoutReady(previewView.get(), countDownLatch);
+ });
+
+ // Assert: OutputTransform is null.
+ assertThat(countDownLatch.await(1, TimeUnit.SECONDS)).isTrue();
+ mInstrumentation.runOnMainSync(
+ () -> assertThat(previewView.get().getOutputTransform()).isNull());
+ }
+
+ @Test
public void previewViewPinched_pinchToZoomInvokedOnController()
throws InterruptedException, UiObjectNotFoundException {
// TODO(b/169058735): investigate and enable on Cuttlefish.
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/transform/CoordinateTransformDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/transform/CoordinateTransformDeviceTest.kt
index 5c5186f..84e21f7 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/transform/CoordinateTransformDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/transform/CoordinateTransformDeviceTest.kt
@@ -35,12 +35,13 @@
@Test(expected = IllegalArgumentException::class)
public fun mismatchViewPort_throwsException() {
// Arrange: create 2 imageProxy with mismatched viewport aspect ratio.
- val source = ImageProxyTransform.Builder(
- createFakeImageProxy(300, 400, Rect(0, 0, 300, 400))
- ).build()
- val target = ImageProxyTransform.Builder(
- createFakeImageProxy(300, 400, Rect(0, 0, 200, 400))
- ).build()
+ val imageProxyTransformFactory = ImageProxyTransformFactory.Builder().build()
+ val source = imageProxyTransformFactory.getOutputTransform(
+ createFakeImageProxy(300, 400, 0, Rect(0, 0, 300, 400))
+ )
+ val target = imageProxyTransformFactory.getOutputTransform(
+ createFakeImageProxy(300, 400, 0, Rect(0, 0, 200, 400))
+ )
// Act: creating CoordinateTransform throws exception.
CoordinateTransform(source, target)
@@ -49,9 +50,10 @@
@Test
public fun sameSourceAndTarget_getsIdentityMatrix() {
// Arrange.
- val imageProxy = ImageProxyTransform.Builder(
- createFakeImageProxy(3, 4, Rect(0, 0, 3, 4))
- ).build()
+ val imageProxyTransformFactory = ImageProxyTransformFactory.Builder().build()
+ val imageProxy = imageProxyTransformFactory.getOutputTransform(
+ createFakeImageProxy(3, 4, 0, Rect(0, 0, 3, 4))
+ )
// Act: create a transform with the same source and target.
val transform = CoordinateTransform(imageProxy, imageProxy)
@@ -68,12 +70,13 @@
@Test
public fun scaleImageProxy() {
// Arrange: create 2 ImageProxy with the only difference being 10x scale.
- val source = ImageProxyTransform.Builder(
- createFakeImageProxy(3, 4, Rect(0, 0, 3, 4))
- ).build()
- val target = ImageProxyTransform.Builder(
- createFakeImageProxy(30, 40, Rect(0, 0, 30, 40))
- ).build()
+ val imageProxyTransformFactory = ImageProxyTransformFactory.Builder().build()
+ val source = imageProxyTransformFactory.getOutputTransform(
+ createFakeImageProxy(3, 4, 0, Rect(0, 0, 3, 4))
+ )
+ val target = imageProxyTransformFactory.getOutputTransform(
+ createFakeImageProxy(30, 40, 0, Rect(0, 0, 30, 40))
+ )
// Act.
val coordinateTransform = CoordinateTransform(source, target)
@@ -87,12 +90,14 @@
@Test
public fun scaleAndRotateImageProxy() {
// Arrange: create 2 ImageProxy with different scale and rotation.
- val source = ImageProxyTransform.Builder(
- createFakeImageProxy(3, 4, Rect(0, 0, 3, 4))
- ).setRotationDegrees(270).build()
- val target = ImageProxyTransform.Builder(
- createFakeImageProxy(30, 40, Rect(0, 0, 30, 40))
- ).setRotationDegrees(90).build()
+ val imageProxyTransformFactory = ImageProxyTransformFactory.Builder()
+ .setUseRotationDegrees(true).build()
+ val source = imageProxyTransformFactory.getOutputTransform(
+ createFakeImageProxy(3, 4, 270, Rect(0, 0, 3, 4))
+ )
+ val target = imageProxyTransformFactory.getOutputTransform(
+ createFakeImageProxy(30, 40, 90, Rect(0, 0, 30, 40))
+ )
// Act.
val coordinateTransform = CoordinateTransform(source, target)
@@ -107,12 +112,14 @@
public fun withViewPortWithoutCropRect() {
// Arrange: create 2 ImageProxy that have crop rect, but the coordinates do not respect the
// crop rect. (MLKit scenario).
- val source = ImageProxyTransform.Builder(
- createFakeImageProxy(16, 12, Rect(2, 2, 10, 8))
- ).build()
- val target = ImageProxyTransform.Builder(
- createFakeImageProxy(16, 12, Rect(8, 6, 16, 12))
- ).build()
+ val imageProxyTransformFactory = ImageProxyTransformFactory.Builder()
+ .setUseRotationDegrees(true).build()
+ val source = imageProxyTransformFactory.getOutputTransform(
+ createFakeImageProxy(16, 12, 0, Rect(2, 2, 10, 8))
+ )
+ val target = imageProxyTransformFactory.getOutputTransform(
+ createFakeImageProxy(16, 12, 0, Rect(8, 6, 16, 12))
+ )
// Act.
val coordinateTransform = CoordinateTransform(source, target)
@@ -127,14 +134,18 @@
public fun withViewPortAndCropRect() {
// Arrange: create 2 ImageProxy that have crop rect, and the coordinates respect the crop
// rect.
- val sourceCropRect = Rect(2, 2, 10, 8)
- val source = ImageProxyTransform.Builder(createFakeImageProxy(16, 12, sourceCropRect))
- .setCropRect(sourceCropRect).build()
- val targetCropRect = Rect(8, 6, 16, 12)
- val target = ImageProxyTransform
- .Builder(createFakeImageProxy(16, 12, targetCropRect))
- .setCropRect(targetCropRect)
- .build()
+ val imageProxyTransformFactory = ImageProxyTransformFactory.Builder()
+ .setUseCropRect(true).build()
+ val source = imageProxyTransformFactory.getOutputTransform(
+ createFakeImageProxy(
+ 16, 12, 0, Rect(2, 2, 10, 8)
+ )
+ )
+ val target = imageProxyTransformFactory.getOutputTransform(
+ createFakeImageProxy(
+ 16, 12, 90, Rect(8, 6, 16, 12)
+ )
+ )
// Act.
val coordinateTransform = CoordinateTransform(source, target)
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/transform/ImageProxyTransformDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/transform/ImageProxyTransformFactoryTest.kt
similarity index 64%
rename from camera/camera-view/src/androidTest/java/androidx/camera/view/transform/ImageProxyTransformDeviceTest.kt
rename to camera/camera-view/src/androidTest/java/androidx/camera/view/transform/ImageProxyTransformFactoryTest.kt
index 43a84ec..dec0cd4 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/transform/ImageProxyTransformDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/transform/ImageProxyTransformFactoryTest.kt
@@ -25,11 +25,11 @@
import org.junit.runner.RunWith
/**
- * Instrument tests for [ImageProxyTransform]
+ * Instrument tests for [ImageProxyTransformFactory]
*/
@LargeTest
@RunWith(AndroidJUnit4::class)
-public class ImageProxyTransformDeviceTest {
+public class ImageProxyTransformFactoryTest {
@Test
public fun rotateVerticesAndAlignToOrigin() {
@@ -37,7 +37,7 @@
val vertices = floatArrayOf(1f, 2f, 4f, 2f, 4f, 6f, 1f, 6f)
// Act.
- val rotatedVertices = ImageProxyTransform.Builder.getRotatedVertices(vertices, 90)
+ val rotatedVertices = ImageProxyTransformFactory.getRotatedVertices(vertices, 90)
// Assert: the rotated rect becomes 4x3 and aligned to the origin: (0,0) - (4,3)
assertThat(rotatedVertices).isEqualTo(floatArrayOf(4f, 0f, 4f, 3f, 0f, 3f, 0f, 0f))
@@ -46,8 +46,9 @@
@Test
public fun withoutRotationOrCropRect_scaled() {
// Arrange: a 3x4 rect.
- val imageProxy = createFakeImageProxy(3, 4, Rect(0, 0, 3, 4))
- val transform = ImageProxyTransform.Builder(imageProxy).build()
+ val imageProxy = createFakeImageProxy(3, 4, 90, Rect(0, 0, 3, 4))
+ val imageProxyTransformFactory = ImageProxyTransformFactory.Builder().build()
+ val transform = imageProxyTransformFactory.getOutputTransform(imageProxy)
// Assert: The bottom-right of the normalized space (1, 1) mapped to (3, 4)
val point = floatArrayOf(1f, 1f)
@@ -58,8 +59,12 @@
@Test
public fun withRotation_scaledAndRotated() {
// Arrange: a 3x4 rect with 90° rotation.
- val imageProxy = createFakeImageProxy(3, 4, Rect(0, 0, 3, 4))
- val transform = ImageProxyTransform.Builder(imageProxy).setRotationDegrees(90).build()
+ // (the MLKit scenario).
+ val imageProxyTransformFactory = ImageProxyTransformFactory.Builder()
+ .setUseRotationDegrees(true)
+ .build()
+ val imageProxy = createFakeImageProxy(3, 4, 90, Rect(0, 0, 3, 4))
+ val transform = imageProxyTransformFactory.getOutputTransform(imageProxy)
// Assert: The bottom-right of the normalized space (1, 1) mapped to (0, 3)
val point = floatArrayOf(1f, 1f)
@@ -70,9 +75,11 @@
@Test
public fun withCropRect_cropped() {
// Arrange: a 16x12 rect with a 8x12 crop rect (8,0)-(16,12).
- val cropRect = Rect(8, 0, 16, 12)
- val imageProxy = createFakeImageProxy(16, 12, cropRect)
- val transform = ImageProxyTransform.Builder(imageProxy).setCropRect(cropRect).build()
+ val imageProxyTransformFactory = ImageProxyTransformFactory.Builder()
+ .setUseCropRect(true)
+ .build()
+ val imageProxy = createFakeImageProxy(16, 12, 90, Rect(8, 0, 16, 12))
+ val transform = imageProxyTransformFactory.getOutputTransform(imageProxy)
// Assert: the center of the normalized space (0.5, 0.5) mapped to the center of the crop
// rect (4,6).
@@ -82,29 +89,14 @@
}
@Test
- public fun withIgnoredCropRect() {
- // The ImageProxy has crop rect which is the viewport, but the user chooses to ignore it
- // (the MLKit scenario).
- // Arrange: a 16x12 rect with a 8x12 crop rect (8,0)-(16,12).
- val imageProxy = createFakeImageProxy(16, 12, Rect(8, 0, 16, 12))
- val transform = ImageProxyTransform.Builder(imageProxy).build()
-
- // Assert: the center of the normalized space (0.5, 0.5) mapped to the center of the crop
- // rect, but the coordinate system is based on the full buffer.
- val point = floatArrayOf(.5f, .5f)
- transform.matrix.mapPoints(point)
- assertThat(point).isEqualTo(floatArrayOf(12f, 6f))
- }
-
- @Test
public fun rotationAndCrop() {
// Arrange: crop rect with rotation.
- val cropRect = Rect(8, 0, 16, 12)
- val imageProxy = createFakeImageProxy(16, 12, cropRect)
- val transform = ImageProxyTransform.Builder(imageProxy)
- .setCropRect(cropRect)
- .setRotationDegrees(90)
+ val imageProxyTransformFactory = ImageProxyTransformFactory.Builder()
+ .setUseCropRect(true)
+ .setUseRotationDegrees(true)
.build()
+ val imageProxy = createFakeImageProxy(16, 12, 90, Rect(8, 0, 16, 12))
+ val transform = imageProxyTransformFactory.getOutputTransform(imageProxy)
// Assert: the center of the normalized space (0.5, 0.5) mapped to the center of the
// rotated crop rect (4,6).
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/transform/TransformTestUtils.java b/camera/camera-view/src/androidTest/java/androidx/camera/view/transform/TransformTestUtils.java
index 8e3e294..1843b7a 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/transform/TransformTestUtils.java
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/transform/TransformTestUtils.java
@@ -28,8 +28,11 @@
*/
class TransformTestUtils {
- static ImageProxy createFakeImageProxy(int width, int height, Rect cropRect) {
- FakeImageProxy imageProxy = new FakeImageProxy(new FakeImageInfo());
+ static ImageProxy createFakeImageProxy(int width, int height,
+ int rotationDegrees, Rect cropRect) {
+ FakeImageInfo fakeImageInfo = new FakeImageInfo();
+ fakeImageInfo.setRotationDegrees(rotationDegrees);
+ FakeImageProxy imageProxy = new FakeImageProxy(fakeImageInfo);
imageProxy.setHeight(height);
imageProxy.setWidth(width);
imageProxy.setCropRect(cropRect);
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java b/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
index b0726fd..8aa80f9 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
@@ -54,8 +54,8 @@
import androidx.camera.core.Logger;
import androidx.camera.core.SurfaceRequest;
import androidx.camera.core.ViewPort;
-import androidx.camera.core.internal.compat.quirk.DeviceQuirks;
-import androidx.camera.core.internal.compat.quirk.PreviewStretchedQuirk;
+import androidx.camera.view.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.view.internal.compat.quirk.PreviewStretchedQuirk;
import androidx.core.util.Preconditions;
/**
@@ -227,8 +227,7 @@
*
* <p> The calculation is based on making the crop rect to fill or fit the {@link PreviewView}.
*/
- private Matrix getSurfaceToPreviewViewMatrix(Size previewViewSize,
- int layoutDirection) {
+ Matrix getSurfaceToPreviewViewMatrix(Size previewViewSize, int layoutDirection) {
Preconditions.checkState(isTransformationInfoReady());
Matrix matrix = new Matrix();
@@ -397,6 +396,14 @@
}
/**
+ * Return the crop rect of the preview surface.
+ */
+ @Nullable
+ Rect getSurfaceCropRect() {
+ return mSurfaceCropRect;
+ }
+
+ /**
* Creates a transformed screenshot of {@link PreviewView}.
*
* <p> Creates the transformed {@link Bitmap} by applying the same transformation applied to
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java b/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
index 90ed0e6..fcf66b48 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
@@ -16,11 +16,15 @@
package androidx.camera.view;
+import static androidx.camera.view.transform.OutputTransform.getNormalizedToBuffer;
+
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Rect;
import android.hardware.camera2.CameraCharacteristics;
import android.os.Build;
import android.util.AttributeSet;
@@ -40,6 +44,7 @@
import androidx.annotation.ColorRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.experimental.UseExperimental;
@@ -60,6 +65,8 @@
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.utils.Threads;
+import androidx.camera.view.transform.CoordinateTransform;
+import androidx.camera.view.transform.OutputTransform;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
@@ -827,6 +834,57 @@
return mCameraController;
}
+ /**
+ * Gets the {@link OutputTransform} associated with the {@link PreviewView}.
+ *
+ * <p> Returns a {@link OutputTransform} object that represents the transform being applied to
+ * the associated {@link Preview} use case. Returns null if the transform info is not ready.
+ * For example, when the associated {@link Preview} has not been bound or the
+ * {@link PreviewView}'s layout is not ready.
+ *
+ * <p> {@link PreviewView} needs to be in {@link ImplementationMode#COMPATIBLE} mode for the
+ * transform to work correctly. For example, the returned {@link OutputTransform} may
+ * not respect the value of {@link #getScaleX()} when {@link ImplementationMode#PERFORMANCE}
+ * mode is used.
+ *
+ * @return the transform applied on the preview by this {@link PreviewView}.
+ * @hide
+ * @see CoordinateTransform
+ */
+ // TODO(b/179827713): unhide this once all transform utils are done.
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @TransformExperimental
+ @Nullable
+ public OutputTransform getOutputTransform() {
+ Threads.checkMainThread();
+ Matrix matrix = null;
+ try {
+ matrix = mPreviewTransform.getSurfaceToPreviewViewMatrix(
+ new Size(getWidth(), getHeight()), getLayoutDirection());
+ } catch (IllegalStateException ex) {
+ // Fall-through. It will be handled below.
+ }
+
+ Rect surfaceCropRect = mPreviewTransform.getSurfaceCropRect();
+ if (matrix == null || surfaceCropRect == null) {
+ Logger.d(TAG, "Transform info is not ready");
+ return null;
+ }
+ // Map it to the normalized space (0, 0) - (1, 1).
+ matrix.preConcat(getNormalizedToBuffer(surfaceCropRect));
+
+ // Add the custom transform applied by the app. e.g. View#setScaleX.
+ if (mImplementation instanceof TextureViewImplementation) {
+ matrix.postConcat(getMatrix());
+ } else {
+ Logger.w(TAG, "PreviewView needs to be in COMPATIBLE mode for the transform"
+ + " to work correctly.");
+ }
+
+ return new OutputTransform(matrix, new Size(surfaceCropRect.width(),
+ surfaceCropRect.height()));
+ }
+
@UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
private void attachToControllerIfReady(boolean shouldFailSilently) {
Display display = getDisplay();
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/TransformUtils.java b/camera/camera-view/src/main/java/androidx/camera/view/TransformUtils.java
index 37ccf4f..4e06287 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/TransformUtils.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/TransformUtils.java
@@ -16,6 +16,7 @@
package androidx.camera.view;
+import android.graphics.Rect;
import android.graphics.RectF;
import android.util.Size;
import android.view.Surface;
@@ -74,6 +75,16 @@
}
/**
+ * Gets the size of the {@link Rect}.
+ * @param rect
+ * @return
+ */
+ @NonNull
+ public static Size rectToSize(@NonNull Rect rect) {
+ return new Size(rect.width(), rect.height());
+ }
+
+ /**
* Converts an array of vertices to a {@link RectF}.
*/
@NonNull
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirks.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirks.java
new file mode 100644
index 0000000..3ae984d
--- /dev/null
+++ b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirks.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view.internal.compat.quirk;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.impl.Quirk;
+import androidx.camera.core.impl.Quirks;
+
+/**
+ * Provider of device specific quirks for the view module, which are used for device specific
+ * workarounds.
+ * <p>
+ * Device specific quirks depend on device properties, including the manufacturer
+ * ({@link android.os.Build#MANUFACTURER}), model ({@link android.os.Build#MODEL}) and OS
+ * level ({@link android.os.Build.VERSION#SDK_INT}).
+ * <p>
+ * Device specific quirks are lazily loaded, i.e. They are loaded the first time they're needed.
+ */
+public class DeviceQuirks {
+
+ @NonNull
+ private static final Quirks QUIRKS;
+
+ static {
+ QUIRKS = new Quirks(DeviceQuirksLoader.loadQuirks());
+ }
+
+ private DeviceQuirks() {
+ }
+
+ /**
+ * Retrieves a specific device {@link Quirk} instance given its type.
+ *
+ * @param quirkClass The type of device quirk to retrieve.
+ * @return A device {@link Quirk} instance of the provided type, or {@code null} if it isn't
+ * found.
+ */
+ @Nullable
+ public static <T extends Quirk> T get(@NonNull final Class<T> quirkClass) {
+ return QUIRKS.get(quirkClass);
+ }
+}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
new file mode 100644
index 0000000..65e35b3
--- /dev/null
+++ b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view.internal.compat.quirk;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.impl.Quirk;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Loads all device specific quirks required for the current device
+ */
+public class DeviceQuirksLoader {
+
+ private DeviceQuirksLoader() {
+ }
+
+ /**
+ * Goes through all defined device-specific quirks, and returns those that should be loaded
+ * on the current device.
+ */
+ @NonNull
+ static List<Quirk> loadQuirks() {
+ final List<Quirk> quirks = new ArrayList<>();
+
+ // Load all device specific quirks
+ if (PreviewStretchedQuirk.load()) {
+ quirks.add(new PreviewStretchedQuirk());
+ }
+
+ return quirks;
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/PreviewStretchedQuirk.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirk.java
similarity index 97%
rename from camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/PreviewStretchedQuirk.java
rename to camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirk.java
index 4d1565b..fd274fa 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/PreviewStretchedQuirk.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirk.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.camera.core.internal.compat.quirk;
+package androidx.camera.view.internal.compat.quirk;
import android.os.Build;
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/package-info.java
similarity index 78%
copy from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
copy to camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/package-info.java
index 7e01354..435388f 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
+++ b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/package-info.java
@@ -1,5 +1,3 @@
-// ktlint-disable filename
-
/*
* Copyright 2021 The Android Open Source Project
*
@@ -16,6 +14,10 @@
* limitations under the License.
*/
-package androidx.compose.foundation
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.camera.view.internal.compat.quirk;
-internal actual typealias AtomicReference<V> = java.util.concurrent.atomic.AtomicReference<V>
\ No newline at end of file
+import androidx.annotation.RestrictTo;
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/transform/ImageProxyTransform.java b/camera/camera-view/src/main/java/androidx/camera/view/transform/ImageProxyTransform.java
deleted file mode 100644
index 0be0ab7..0000000
--- a/camera/camera-view/src/main/java/androidx/camera/view/transform/ImageProxyTransform.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.view.transform;
-
-import static androidx.camera.view.TransformUtils.min;
-import static androidx.camera.view.TransformUtils.rectToVertices;
-
-import android.graphics.Matrix;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.util.Size;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.camera.core.ImageAnalysis;
-import androidx.camera.core.ImageCapture;
-import androidx.camera.core.ImageProxy;
-import androidx.camera.view.TransformExperimental;
-
-/**
- * The transform of a {@link ImageProxy}.
- *
- * <p> {@link ImageProxy} can be the output of {@link ImageAnalysis} or in-memory
- * {@link ImageCapture}. This class represents the transform applied to the raw buffer of a
- * {@link ImageProxy}.
- *
- * TODO(b/179827713): unhide this class once all transform utils are done.
- *
- * @hide
- */
-@TransformExperimental
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class ImageProxyTransform extends OutputTransform {
-
- ImageProxyTransform(@NonNull Matrix matrix, @NonNull Rect viewPortRect) {
- super(matrix, new Size(viewPortRect.width(), viewPortRect.height()));
- }
-
- /**
- * Builder of {@link ImageProxyTransform}.
- */
- public static class Builder extends OutputTransform.Builder {
-
- private final Rect mViewPortRect;
- private int mRotationDegrees;
- private Rect mCropRect;
-
- /**
- * @param imageProxy the {@link ImageProxy} that the transform applies to.
- */
- public Builder(@NonNull ImageProxy imageProxy) {
- mViewPortRect = imageProxy.getCropRect();
- mCropRect = new Rect(0, 0, imageProxy.getWidth(), imageProxy.getHeight());
- mRotationDegrees = 0;
- }
-
- /**
- * Sets the crop rect.
- *
- * <p> Only sets this value if the coordinates to be transformed respect the crop
- * rect, for example, the origin of the coordinates system is the (top, left) of the crop
- * rect.
- */
- @NonNull
- public Builder setCropRect(@NonNull Rect cropRect) {
- mCropRect = cropRect;
- return this;
- }
-
- /**
- * Sets the rotation degrees.
- *
- * <p> Only sets this value if the coordinates to be transformed respect the rotation
- * degrees.
- */
- @NonNull
- public Builder setRotationDegrees(int rotationDegrees) {
- mRotationDegrees = rotationDegrees;
- return this;
- }
-
- // TODO(b/179827713): Support mirroring.
-
- /**
- * Builds the {@link ImageProxyTransform} object.
- */
- @NonNull
- public ImageProxyTransform build() {
- Matrix matrix = new Matrix();
-
- // Map the viewport to output.
- float[] cropRectVertices = rectToVertices(new RectF(mCropRect));
- float[] outputVertices = getRotatedVertices(cropRectVertices, mRotationDegrees);
- matrix.setPolyToPoly(cropRectVertices, 0, outputVertices, 0, 4);
-
- // Map the normalized space to viewport.
- matrix.preConcat(getNormalizedToBuffer(mViewPortRect));
-
- return new ImageProxyTransform(matrix, mViewPortRect);
- }
-
- /**
- * Rotates the crop rect with given degrees.
- *
- * <p> Rotate the vertices, then align the top left corner to (0, 0).
- *
- * <pre>
- * (0, 0) (0, 0)
- * Before +-----Surface-----+ After: a--------------------b
- * | | | ^ |
- * | d-crop rect-a | | | |
- * | | | | d--------------------c
- * | | | |
- * | | --> | | Rotation: <-----+
- * | | | | 270°|
- * | | | | |
- * | c-----------b |
- * +-----------------+
- * </pre>
- */
- static float[] getRotatedVertices(float[] cropRectVertices, int rotationDegrees) {
- // Rotate the vertices. The pivot point doesn't matter since we are gong to align it to
- // the origin afterwards.
- float[] vertices = cropRectVertices.clone();
- Matrix matrix = new Matrix();
- matrix.setRotate(rotationDegrees);
- matrix.mapPoints(vertices);
-
- // Align the rotated vertices to origin. The transformed output always starts at (0, 0).
- float left = min(vertices[0], vertices[2], vertices[4], vertices[6]);
- float top = min(vertices[1], vertices[3], vertices[5], vertices[7]);
- for (int i = 0; i < vertices.length; i += 2) {
- vertices[i] -= left;
- vertices[i + 1] -= top;
- }
- return vertices;
- }
- }
-}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/transform/ImageProxyTransformFactory.java b/camera/camera-view/src/main/java/androidx/camera/view/transform/ImageProxyTransformFactory.java
new file mode 100644
index 0000000..06e422a
--- /dev/null
+++ b/camera/camera-view/src/main/java/androidx/camera/view/transform/ImageProxyTransformFactory.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view.transform;
+
+import static androidx.camera.view.TransformUtils.min;
+import static androidx.camera.view.TransformUtils.rectToSize;
+import static androidx.camera.view.TransformUtils.rectToVertices;
+import static androidx.camera.view.transform.OutputTransform.getNormalizedToBuffer;
+
+import android.graphics.Matrix;
+import android.graphics.RectF;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.camera.core.ImageAnalysis;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.view.TransformExperimental;
+
+/**
+ * Factory for extracting transform info from {@link ImageProxy}.
+ *
+ * TODO(b/179827713): unhide this class once all transform utils are done.
+ *
+ * @hide
+ */
+@TransformExperimental
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class ImageProxyTransformFactory {
+
+ private final boolean mUseCropRect;
+ private final boolean mUseRotationDegrees;
+
+ ImageProxyTransformFactory(boolean useCropRect, boolean useRotationDegrees) {
+ mUseCropRect = useCropRect;
+ mUseRotationDegrees = useRotationDegrees;
+ }
+
+ /**
+ * Extracts the transform from the given {@link ImageProxy}.
+ *
+ * <p> This method returns a {@link OutputTransform} that represents the
+ * transform applied to the buffer of a {@link ImageProxy} based on factory settings. An
+ * {@link ImageProxy} can be the output of {@link ImageAnalysis} or in-memory
+ * {@link ImageCapture}.
+ */
+ @NonNull
+ public OutputTransform getOutputTransform(@NonNull ImageProxy imageProxy) {
+ Matrix matrix = new Matrix();
+
+ // Map the viewport to output.
+ float[] cropRectVertices = rectToVertices(getCropRect(imageProxy));
+ float[] outputVertices = getRotatedVertices(cropRectVertices,
+ getRotationDegrees(imageProxy));
+ matrix.setPolyToPoly(cropRectVertices, 0, outputVertices, 0, 4);
+
+ // Map the normalized space to viewport.
+ matrix.preConcat(getNormalizedToBuffer(imageProxy.getCropRect()));
+
+ return new OutputTransform(matrix, rectToSize(imageProxy.getCropRect()));
+ }
+
+ /**
+ * Gets the crop rect based on factory settings.
+ */
+ private RectF getCropRect(@NonNull ImageProxy imageProxy) {
+ if (mUseCropRect) {
+ return new RectF(imageProxy.getCropRect());
+ }
+ // The default crop rect is the full buffer.
+ return new RectF(0, 0, imageProxy.getWidth(), imageProxy.getHeight());
+ }
+
+ /**
+ * Gets the rotation degrees based on factory settings.
+ */
+ private int getRotationDegrees(@NonNull ImageProxy imageProxy) {
+ if (mUseRotationDegrees) {
+ return imageProxy.getImageInfo().getRotationDegrees();
+ }
+ // The default is no rotation.
+ return 0;
+ }
+
+ /**
+ * Rotates the crop rect with given degrees.
+ *
+ * <p> Rotate the vertices, then align the top left corner to (0, 0).
+ *
+ * <pre>
+ * (0, 0) (0, 0)
+ * Before +-----Surface-----+ After: a--------------------b
+ * | | | ^ |
+ * | d-crop rect-a | | | |
+ * | | | | d--------------------c
+ * | | | |
+ * | | --> | | Rotation: <-----+
+ * | | | | 270°|
+ * | | | | |
+ * | c-----------b |
+ * +-----------------+
+ * </pre>
+ */
+ static float[] getRotatedVertices(float[] cropRectVertices, int rotationDegrees) {
+ // Rotate the vertices. The pivot point doesn't matter since we are gong to align it to
+ // the origin afterwards.
+ float[] vertices = cropRectVertices.clone();
+ Matrix matrix = new Matrix();
+ matrix.setRotate(rotationDegrees);
+ matrix.mapPoints(vertices);
+
+ // Align the rotated vertices to origin. The transformed output always starts at (0, 0).
+ float left = min(vertices[0], vertices[2], vertices[4], vertices[6]);
+ float top = min(vertices[1], vertices[3], vertices[5], vertices[7]);
+ for (int i = 0; i < vertices.length; i += 2) {
+ vertices[i] -= left;
+ vertices[i + 1] -= top;
+ }
+ return vertices;
+ }
+
+ /**
+ * Builder of {@link ImageProxyTransformFactory}.
+ */
+ public static class Builder {
+
+ private boolean mUseCropRect = false;
+ private boolean mUseRotationDegrees = false;
+
+ /**
+ * Whether to use the crop rect of the {@link ImageProxy}.
+ *
+ * <p> By default, the value is false and the factory uses the {@link ImageProxy}'s
+ * entire buffer. Only set this value if the coordinates to be transformed respect the
+ * crop rect. For example, top-left corner of the crop rect is (0, 0).
+ */
+ @NonNull
+ public Builder setUseCropRect(boolean useCropRect) {
+ mUseCropRect = useCropRect;
+ return this;
+ }
+
+ /**
+ * Whether to use the rotation degrees of the {@link ImageProxy}.
+ *
+ * <p> By default, the value is false and the factory uses a rotation degree of 0. Only
+ * set this value if the coordinates to be transformed respect the rotation degrees. For
+ * example, if rotation is 90°, (0, 0) should map to (0, height) on the buffer.
+ */
+ @NonNull
+ public Builder setUseRotationDegrees(boolean useRotationDegrees) {
+ mUseRotationDegrees = useRotationDegrees;
+ return this;
+ }
+
+ // TODO(b/179827713): Add support for mirroring.
+
+ /**
+ * Builds the {@link ImageProxyTransformFactory} object.
+ */
+ @NonNull
+ public ImageProxyTransformFactory build() {
+ return new ImageProxyTransformFactory(mUseCropRect, mUseRotationDegrees);
+ }
+ }
+}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/transform/OutputTransform.java b/camera/camera-view/src/main/java/androidx/camera/view/transform/OutputTransform.java
index cc31198..4011e8e6 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/transform/OutputTransform.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/transform/OutputTransform.java
@@ -39,7 +39,10 @@
*/
@TransformExperimental
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public abstract class OutputTransform {
+public class OutputTransform {
+
+ // Normalized space that maps to the viewport rect.
+ private static final RectF NORMALIZED_RECT = new RectF(0, 0, 1, 1);
@NonNull
final Matrix mMatrix;
@@ -57,8 +60,10 @@
* other {@link OutputTransform}, we can at least make sure that they
* have the same aspect ratio. Viewports with different aspect ratios
* cannot be from the same {@link UseCaseGroup}.
+ * @hide
*/
- OutputTransform(@NonNull Matrix matrix, @NonNull Size viewPortSize) {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public OutputTransform(@NonNull Matrix matrix, @NonNull Size viewPortSize) {
mMatrix = matrix;
mViewPortSize = viewPortSize;
}
@@ -74,26 +79,21 @@
}
/**
- * Abstract builder of {@link OutputTransform} that provides shared functionalities.
+ * @hide
*/
- static class Builder {
+ @NonNull
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static Matrix getNormalizedToBuffer(@NonNull Rect viewPortRect) {
+ return getNormalizedToBuffer(new RectF(viewPortRect));
+ }
- // Normalized space that maps to the viewport rect.
- private static final RectF NORMALIZED_RECT = new RectF(0, 0, 1, 1);
-
- @NonNull
- Matrix getNormalizedToBuffer(@NonNull Rect viewPortRect) {
- return getNormalizedToBuffer(new RectF(viewPortRect));
- }
-
- /**
- * Gets the transform from a normalized space (0, 0) - (1, 1) to viewport rect.
- */
- @NonNull
- Matrix getNormalizedToBuffer(@NonNull RectF viewPortRect) {
- Matrix normalizedToBuffer = new Matrix();
- normalizedToBuffer.setRectToRect(NORMALIZED_RECT, viewPortRect, Matrix.ScaleToFit.FILL);
- return normalizedToBuffer;
- }
+ /**
+ * Gets the transform from a normalized space (0, 0) - (1, 1) to viewport rect.
+ */
+ @NonNull
+ static Matrix getNormalizedToBuffer(@NonNull RectF viewPortRect) {
+ Matrix normalizedToBuffer = new Matrix();
+ normalizedToBuffer.setRectToRect(NORMALIZED_RECT, viewPortRect, Matrix.ScaleToFit.FILL);
+ return normalizedToBuffer;
}
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/quirk/PreviewStretchedQuirkTest.java b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirkTest.java
similarity index 96%
rename from camera/camera-core/src/test/java/androidx/camera/core/internal/compat/quirk/PreviewStretchedQuirkTest.java
rename to camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirkTest.java
index 1b126ad..0b42944 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/quirk/PreviewStretchedQuirkTest.java
+++ b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirkTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.camera.core.internal.compat.quirk;
+package androidx.camera.view.internal.compat.quirk;
import static com.google.common.truth.Truth.assertThat;
diff --git a/camera/integration-tests/coretestapp/build.gradle b/camera/integration-tests/coretestapp/build.gradle
index 65a474e..1fd48a3 100644
--- a/camera/integration-tests/coretestapp/build.gradle
+++ b/camera/integration-tests/coretestapp/build.gradle
@@ -70,7 +70,8 @@
implementation(project(":appcompat:appcompat"))
implementation("androidx.activity:activity:1.2.0")
implementation("androidx.fragment:fragment:1.3.0")
- implementation("androidx.concurrent:concurrent-futures:1.0.0")
+ // Needed because AGP enforces same version between main and androidTest classpaths
+ implementation(project(":concurrent:concurrent-futures"))
// Android Support Library
api(CONSTRAINT_LAYOUT, { transitive = true })
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/CameraUtils.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/CameraUtils.kt
index 6bcc17c..9c5e7f9 100644
--- a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/CameraUtils.kt
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/CameraUtils.kt
@@ -75,8 +75,9 @@
val manager = activity.getSystemService(AppCompatActivity.CAMERA_SERVICE) as CameraManager
try {
val numCameras = manager.cameraIdList.size
-
for (cameraId in manager.cameraIdList) {
+ var cameraParamsValid = true
+
val tempCameraParams = CameraParams().apply {
val cameraChars = manager.getCameraCharacteristics(cameraId)
@@ -84,6 +85,17 @@
cameraChars.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)
?: IntArray(0)
+ // Check supported format.
+ val map = cameraChars.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
+ if (map == null || map.isOutputSupportedFor(ImageFormat.JPEG) == false) {
+ cameraParamsValid = false
+ logd(
+ "Null streamConfigurationMap or not supporting JPEG output format " +
+ "in cameraId:" + cameraId
+ )
+ return@apply
+ }
+
// Multi-camera
for (capability in cameraCapabilities) {
if (capability ==
@@ -178,27 +190,29 @@
physicalCameras = cameraChars.physicalCameraIds
}
- // Get Camera2 and CameraX image capture sizes
- val map =
- characteristics?.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
- if (map != null) {
- cam2MaxSize = Collections.max(
- Arrays.asList(*map.getOutputSizes(ImageFormat.JPEG)),
- CompareSizesByArea()
- )
- cam2MinSize = Collections.min(
- Arrays.asList(*map.getOutputSizes(ImageFormat.JPEG)),
- CompareSizesByArea()
- )
+ // Get Camera2 and CameraX image capture sizes.
+ cam2MaxSize = Collections.max(
+ Arrays.asList(*map.getOutputSizes(ImageFormat.JPEG)),
+ CompareSizesByArea()
+ )
- // Use minimum image size for preview
- previewSurfaceView?.holder?.setFixedSize(cam2MinSize.width, cam2MinSize.height)
- }
+ cam2MinSize = Collections.min(
+ Arrays.asList(*map.getOutputSizes(ImageFormat.JPEG)),
+ CompareSizesByArea()
+ )
+
+ // Use minimum image size for preview
+ previewSurfaceView?.holder?.setFixedSize(cam2MinSize.width, cam2MinSize.height)
setupImageReader(activity, this, TestConfig())
}
- cameraParams.put(cameraId, tempCameraParams)
+ if (cameraParamsValid == false) {
+ logd("Don't put Camera " + cameraId + "of " + numCameras)
+ continue
+ } else {
+ cameraParams.put(cameraId, tempCameraParams)
+ }
} // For all camera devices
} catch (accessError: CameraAccessException) {
accessError.printStackTrace()
diff --git a/camera/integration-tests/viewtestapp/build.gradle b/camera/integration-tests/viewtestapp/build.gradle
index 21c0089..0a85291 100644
--- a/camera/integration-tests/viewtestapp/build.gradle
+++ b/camera/integration-tests/viewtestapp/build.gradle
@@ -23,6 +23,8 @@
id("kotlin-android")
}
+apply(from: "../../camera-view/dependencies.gradle")
+
android {
defaultConfig {
applicationId "androidx.camera.integration.view"
@@ -51,8 +53,8 @@
dependencies {
// Internal library
- implementation(project(":camera:camera-camera2"))
- implementation(project(":camera:camera-lifecycle"))
+ implementation("androidx.camera:camera-camera2:${VIEW_ATOMIC_GROUP_PINNED_VER}")
+ implementation("androidx.camera:camera-lifecycle:${VIEW_ATOMIC_GROUP_PINNED_VER}")
implementation(project(":lifecycle:lifecycle-runtime"))
implementation(project(":camera:camera-view"))
implementation(GUAVA_ANDROID)
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java
index c23de8b..762eda3 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java
@@ -122,6 +122,9 @@
case R.id.camera_controller:
mMode = Mode.CAMERA_CONTROLLER;
break;
+ case R.id.transform:
+ mMode = Mode.TRANSFORM;
+ break;
}
startFragment();
return true;
@@ -148,6 +151,8 @@
case CAMERA_CONTROLLER:
startFragment(R.string.camera_controller, new CameraControllerFragment());
break;
+ case TRANSFORM:
+ startFragment(R.string.transform, new TransformFragment());
}
}
@@ -165,6 +170,6 @@
}
private enum Mode {
- CAMERA_VIEW, PREVIEW_VIEW, CAMERA_CONTROLLER
+ CAMERA_VIEW, PREVIEW_VIEW, CAMERA_CONTROLLER, TRANSFORM
}
}
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/OverlayView.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/OverlayView.java
new file mode 100644
index 0000000..614515df
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/OverlayView.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.view;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * A overlay view for drawing a tile with {@link Canvas}.
+ */
+public final class OverlayView extends FrameLayout {
+
+ private RectF mTile;
+ private final Paint mPaint = new Paint();
+
+ public OverlayView(@NonNull Context context) {
+ super(context);
+ }
+
+
+ public OverlayView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ void setTileRect(RectF tile) {
+ mTile = tile;
+ }
+
+ @Override
+ public void onDraw(@NonNull Canvas canvas) {
+ super.onDraw(canvas);
+ if (mTile == null) {
+ return;
+ }
+
+ // The tile paint is black stroke with a white glow so it's always visible regardless of
+ // the background.
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setColor(Color.WHITE);
+ mPaint.setStrokeWidth(10);
+ canvas.drawRect(mTile, mPaint);
+
+ mPaint.setColor(Color.BLACK);
+ mPaint.setStrokeWidth(5);
+ canvas.drawRect(mTile, mPaint);
+ }
+}
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/TransformFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/TransformFragment.java
new file mode 100644
index 0000000..fc44966
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/TransformFragment.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.view;
+
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ToggleButton;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.experimental.UseExperimental;
+import androidx.camera.core.ImageAnalysis;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.view.LifecycleCameraController;
+import androidx.camera.view.PreviewView;
+import androidx.camera.view.TransformExperimental;
+import androidx.camera.view.transform.CoordinateTransform;
+import androidx.camera.view.transform.ImageProxyTransformFactory;
+import androidx.camera.view.transform.OutputTransform;
+import androidx.fragment.app.Fragment;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * A fragment that demos transform utilities.
+ */
+public final class TransformFragment extends Fragment {
+
+ private static final int TILE_COUNT = 4;
+
+ private LifecycleCameraController mCameraController;
+ private ExecutorService mExecutorService;
+ private ToggleButton mMirror;
+
+ // Synthetic access
+ @SuppressWarnings("WeakerAccess")
+ PreviewView mPreviewView;
+ // Synthetic access
+ @SuppressWarnings("WeakerAccess")
+ OverlayView mOverlayView;
+
+ private final ImageAnalysis.Analyzer mAnalyzer = new ImageAnalysis.Analyzer() {
+
+ private final ImageProxyTransformFactory mImageProxyTransformFactory =
+ new ImageProxyTransformFactory.Builder().build();
+
+ @Override
+ @UseExperimental(markerClass = TransformExperimental.class)
+ @SuppressWarnings("RestrictedApi")
+ public void analyze(@NonNull ImageProxy imageProxy) {
+ // Find the tile to highlight.
+ RectF brightestTile = findBrightestTile(imageProxy);
+
+ // Calculate PreviewView transform on UI thread.
+ mOverlayView.post(() -> {
+ // Calculate the transform.
+ try (ImageProxy imageToClose = imageProxy) {
+ OutputTransform previewViewTransform = mPreviewView.getOutputTransform();
+ if (previewViewTransform == null) {
+ // PreviewView transform info is not ready. No-op.
+ return;
+ }
+ CoordinateTransform transform = new CoordinateTransform(
+ mImageProxyTransformFactory.getOutputTransform(imageToClose),
+ previewViewTransform);
+ Matrix analysisToPreview = new Matrix();
+ transform.getTransform(analysisToPreview);
+
+ // Map the tile to PreviewView coordinates.
+ analysisToPreview.mapRect(brightestTile);
+ // Draw the tile on top of PreviewView.
+ mOverlayView.setTileRect(brightestTile);
+ mOverlayView.postInvalidate();
+ }
+ });
+ }
+ };
+
+ /**
+ * Finds the brightest tile in the given {@link ImageProxy}.
+ *
+ * <p> Divides the crop rect of the image into a 4x4 grid, and find the brightest tile
+ * among the 16 tiles.
+ *
+ * @return the box of the brightest tile.
+ */
+ // Synthetic access
+ @SuppressWarnings("WeakerAccess")
+ RectF findBrightestTile(ImageProxy image) {
+ // Divide the crop rect in to 4x4 tiles.
+ Rect cropRect = image.getCropRect();
+ int[][] tiles = new int[TILE_COUNT][TILE_COUNT];
+ int tileWidth = cropRect.width() / TILE_COUNT;
+ int tileHeight = cropRect.height() / TILE_COUNT;
+
+ // Loop through the y plane and get the sum of the luminance for each tile.
+ byte[] bytes = new byte[image.getPlanes()[0].getBuffer().remaining()];
+ image.getPlanes()[0].getBuffer().get(bytes);
+ for (int x = 0; x < cropRect.width(); x++) {
+ for (int y = 0; y < cropRect.height(); y++) {
+ tiles[x / tileWidth][y / tileHeight] +=
+ bytes[(y + cropRect.top) * image.getWidth() + cropRect.left + x] & 0xFF;
+ }
+ }
+
+ // Find the brightest tile among the 16 tiles.
+ float maxLuminance = 0;
+ int brightestTileX = 0;
+ int brightestTileY = 0;
+ for (int i = 0; i < TILE_COUNT; i++) {
+ for (int j = 0; j < TILE_COUNT; j++) {
+ if (tiles[i][j] > maxLuminance) {
+ maxLuminance = tiles[i][j];
+ brightestTileX = i;
+ brightestTileY = j;
+ }
+ }
+ }
+
+ // Return the rectangle of the tile.
+ return new RectF(brightestTileX * tileWidth + cropRect.left,
+ brightestTileY * tileHeight + cropRect.top,
+ (brightestTileX + 1) * tileWidth + cropRect.left,
+ (brightestTileY + 1) * tileHeight + cropRect.top);
+ }
+
+ @NonNull
+ @Override
+ public View onCreateView(
+ @NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ mExecutorService = Executors.newSingleThreadExecutor();
+ mCameraController = new LifecycleCameraController(requireContext());
+ mCameraController.bindToLifecycle(getViewLifecycleOwner());
+
+ View view = inflater.inflate(R.layout.transform_view, container, false);
+
+ mPreviewView = view.findViewById(R.id.preview_view);
+ // Set to compatible so the custom transform (e.g. mirroring) would work.
+ mPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
+ mPreviewView.setController(mCameraController);
+
+ mCameraController.setImageAnalysisAnalyzer(mExecutorService, mAnalyzer);
+
+ mOverlayView = view.findViewById(R.id.overlay_view);
+ mMirror = view.findViewById(R.id.mirror_preview);
+ mMirror.setOnCheckedChangeListener((buttonView, isChecked) -> updateMirrorState());
+
+ updateMirrorState();
+ return view;
+ }
+
+ private void updateMirrorState() {
+ if (mMirror.isChecked()) {
+ mPreviewView.setScaleX(-1);
+ } else {
+ mPreviewView.setScaleX(1);
+ }
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ if (mExecutorService != null) {
+ mExecutorService.shutdown();
+ }
+ }
+}
diff --git a/camera/integration-tests/viewtestapp/src/main/res/layout-land/transform_view.xml b/camera/integration-tests/viewtestapp/src/main/res/layout-land/transform_view.xml
new file mode 100644
index 0000000..f7f80f0
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/src/main/res/layout-land/transform_view.xml
@@ -0,0 +1,47 @@
+<?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.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+ <FrameLayout
+ android:id="@+id/container"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1">
+ <androidx.camera.view.PreviewView
+ android:id="@+id/preview_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+ <androidx.camera.integration.view.OverlayView
+ android:id="@+id/overlay_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="#00000000"/>
+ </FrameLayout>
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent">
+ <ToggleButton
+ android:id="@+id/mirror_preview"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textOff="@string/mirror_off"
+ android:textOn="@string/mirror_on"
+ android:checked="false"/>
+ </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/camera/integration-tests/viewtestapp/src/main/res/layout/transform_view.xml b/camera/integration-tests/viewtestapp/src/main/res/layout/transform_view.xml
new file mode 100644
index 0000000..7c75d7f
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/src/main/res/layout/transform_view.xml
@@ -0,0 +1,47 @@
+<?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.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <FrameLayout
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1">
+ <androidx.camera.view.PreviewView
+ android:id="@+id/preview_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+ <androidx.camera.integration.view.OverlayView
+ android:id="@+id/overlay_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="#00000000"/>
+ </FrameLayout>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <ToggleButton
+ android:id="@+id/mirror_preview"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textOff="@string/mirror_off"
+ android:textOn="@string/mirror_on"
+ android:checked="false"/>
+ </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/camera/integration-tests/viewtestapp/src/main/res/menu/actionbar_menu.xml b/camera/integration-tests/viewtestapp/src/main/res/menu/actionbar_menu.xml
index 52d01f9..8babd1e 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/menu/actionbar_menu.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/menu/actionbar_menu.xml
@@ -28,4 +28,8 @@
android:id="@+id/camera_controller"
android:title="@string/camera_controller"
app:showAsAction="never" />
+ <item
+ android:id="@+id/transform"
+ android:title="@string/transform"
+ app:showAsAction="never" />
</menu>
\ No newline at end of file
diff --git a/camera/integration-tests/viewtestapp/src/main/res/values/strings.xml b/camera/integration-tests/viewtestapp/src/main/res/values/strings.xml
index 7c76a73..adaba7a 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/values/strings.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/values/strings.xml
@@ -43,8 +43,11 @@
<string name="camera_view">Camera View</string>
<string name="preview_view">Preview View</string>
<string name="camera_controller">Camera Controller</string>
+ <string name="transform">Transform</string>
<string name="flash_mode_auto">flash auto</string>
<string name="flash_mode_on">flash on</string>
<string name="flash_mode_off">flash off</string>
+ <string name="mirror_on">mirror on</string>
+ <string name="mirror_off">mirror off</string>
</resources>
diff --git a/car/app/app-testing/api/current.txt b/car/app/app-testing/api/current.txt
index 126bce5..52b6e21 100644
--- a/car/app/app-testing/api/current.txt
+++ b/car/app/app-testing/api/current.txt
@@ -9,6 +9,7 @@
method public androidx.car.app.testing.ScreenController create();
method public androidx.car.app.testing.ScreenController destroy();
method public androidx.car.app.Screen get();
+ method public Object? getScreenResult();
method public java.util.List<androidx.car.app.model.Template!> getTemplatesReturned();
method public static androidx.car.app.testing.ScreenController of(androidx.car.app.testing.TestCarContext, androidx.car.app.Screen);
method public androidx.car.app.testing.ScreenController pause();
diff --git a/car/app/app-testing/api/public_plus_experimental_current.txt b/car/app/app-testing/api/public_plus_experimental_current.txt
index 126bce5..52b6e21 100644
--- a/car/app/app-testing/api/public_plus_experimental_current.txt
+++ b/car/app/app-testing/api/public_plus_experimental_current.txt
@@ -9,6 +9,7 @@
method public androidx.car.app.testing.ScreenController create();
method public androidx.car.app.testing.ScreenController destroy();
method public androidx.car.app.Screen get();
+ method public Object? getScreenResult();
method public java.util.List<androidx.car.app.model.Template!> getTemplatesReturned();
method public static androidx.car.app.testing.ScreenController of(androidx.car.app.testing.TestCarContext, androidx.car.app.Screen);
method public androidx.car.app.testing.ScreenController pause();
diff --git a/car/app/app-testing/api/restricted_current.txt b/car/app/app-testing/api/restricted_current.txt
index 126bce5..52b6e21 100644
--- a/car/app/app-testing/api/restricted_current.txt
+++ b/car/app/app-testing/api/restricted_current.txt
@@ -9,6 +9,7 @@
method public androidx.car.app.testing.ScreenController create();
method public androidx.car.app.testing.ScreenController destroy();
method public androidx.car.app.Screen get();
+ method public Object? getScreenResult();
method public java.util.List<androidx.car.app.model.Template!> getTemplatesReturned();
method public static androidx.car.app.testing.ScreenController of(androidx.car.app.testing.TestCarContext, androidx.car.app.Screen);
method public androidx.car.app.testing.ScreenController pause();
diff --git a/car/app/app-testing/src/main/java/androidx/car/app/testing/ScreenController.java b/car/app/app-testing/src/main/java/androidx/car/app/testing/ScreenController.java
index 60d9140..9812453 100644
--- a/car/app/app-testing/src/main/java/androidx/car/app/testing/ScreenController.java
+++ b/car/app/app-testing/src/main/java/androidx/car/app/testing/ScreenController.java
@@ -22,6 +22,7 @@
import android.util.Pair;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.car.app.Screen;
import androidx.car.app.model.Template;
import androidx.lifecycle.Lifecycle.Event;
@@ -87,6 +88,22 @@
}
/**
+ * Returns the result that was set via {@link Screen#setResult(Object)}, or {@code null} if
+ * one was not set.
+ */
+ @Nullable
+ public Object getScreenResult() {
+ try {
+ Field result = Screen.class.getDeclaredField("mResult");
+ result.setAccessible(true);
+ return result.get(mScreen);
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException("Unable to access result from screen being "
+ + "controlled", e);
+ }
+ }
+
+ /**
* Creates the {@link Screen} being controlled.
*
* <p>This method will also push the {@link Screen} onto the {@link
diff --git a/car/app/app-testing/src/main/java/androidx/car/app/testing/TestAppManager.java b/car/app/app-testing/src/main/java/androidx/car/app/testing/TestAppManager.java
index 35c91f8..6160238 100644
--- a/car/app/app-testing/src/main/java/androidx/car/app/testing/TestAppManager.java
+++ b/car/app/app-testing/src/main/java/androidx/car/app/testing/TestAppManager.java
@@ -129,6 +129,6 @@
}
TestAppManager(TestCarContext testCarContext, HostDispatcher hostDispatcher) {
- super(testCarContext, hostDispatcher);
+ super(testCarContext, hostDispatcher, testCarContext.getLifecycleOwner().mRegistry);
}
}
diff --git a/car/app/app-testing/src/main/java/androidx/car/app/testing/TestCarContext.java b/car/app/app-testing/src/main/java/androidx/car/app/testing/TestCarContext.java
index 30051d1..035ee70 100644
--- a/car/app/app-testing/src/main/java/androidx/car/app/testing/TestCarContext.java
+++ b/car/app/app-testing/src/main/java/androidx/car/app/testing/TestCarContext.java
@@ -183,7 +183,6 @@
*
* @throws NullPointerException if either {@code serviceClass} or {@code service} are {@code
* null}
- *
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@@ -195,7 +194,14 @@
mOverriddenService.put(serviceName, service);
}
- TestLifecycleOwner getLifecycleOwner() {
+ /**
+ * Returns the {@link TestLifecycleOwner} that is used for this CarContext.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @NonNull
+ public TestLifecycleOwner getLifecycleOwner() {
return mTestLifecycleOwner;
}
diff --git a/car/app/app-testing/src/main/java/androidx/car/app/testing/navigation/TestNavigationManager.java b/car/app/app-testing/src/main/java/androidx/car/app/testing/navigation/TestNavigationManager.java
index e668cdd4..6c5f94c 100644
--- a/car/app/app-testing/src/main/java/androidx/car/app/testing/navigation/TestNavigationManager.java
+++ b/car/app/app-testing/src/main/java/androidx/car/app/testing/navigation/TestNavigationManager.java
@@ -132,6 +132,6 @@
public TestNavigationManager(@NonNull TestCarContext testCarContext,
@NonNull HostDispatcher hostDispatcher) {
- super(testCarContext, hostDispatcher);
+ super(testCarContext, hostDispatcher, testCarContext.getLifecycleOwner().mRegistry);
}
}
diff --git a/car/app/app-testing/src/test/java/androidx/car/app/testing/ScreenControllerTest.java b/car/app/app-testing/src/test/java/androidx/car/app/testing/ScreenControllerTest.java
index c64f636..76f4bc0 100644
--- a/car/app/app-testing/src/test/java/androidx/car/app/testing/ScreenControllerTest.java
+++ b/car/app/app-testing/src/test/java/androidx/car/app/testing/ScreenControllerTest.java
@@ -159,6 +159,16 @@
}
@Test
+ public void getScreenResult() {
+ Screen screen = mScreenController.get();
+ String result = "this is the result";
+
+ screen.setResult(result);
+
+ assertThat(mScreenController.getScreenResult()).isEqualTo(result);
+ }
+
+ @Test
public void reset() {
mScreenController.start();
mScreenController.reset();
diff --git a/car/app/app/src/main/java/androidx/car/app/AppManager.java b/car/app/app/src/main/java/androidx/car/app/AppManager.java
index e0038c8..851aa8f 100644
--- a/car/app/app/src/main/java/androidx/car/app/AppManager.java
+++ b/car/app/app/src/main/java/androidx/car/app/AppManager.java
@@ -27,9 +27,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
-import androidx.car.app.model.TemplateWrapper;
import androidx.car.app.utils.RemoteUtils;
-import androidx.car.app.utils.ThreadUtils;
+import androidx.lifecycle.Lifecycle;
/** Manages the communication between the app and the host. */
public class AppManager {
@@ -39,6 +38,8 @@
private final IAppManager.Stub mAppManager;
@NonNull
private final HostDispatcher mHostDispatcher;
+ @NonNull
+ private final Lifecycle mLifecycle;
/**
* Sets the {@link SurfaceCallback} to get changes and updates to the surface on which the
@@ -62,11 +63,12 @@
public void setSurfaceCallback(@Nullable SurfaceCallback surfaceCallback) {
mHostDispatcher.dispatch(
CarContext.APP_SERVICE,
- (IAppHost host) -> {
- host.setSurfaceCallback(RemoteUtils.stubSurfaceCallback(surfaceCallback));
+ "setSurfaceListener", (IAppHost host) -> {
+ host.setSurfaceCallback(
+ RemoteUtils.stubSurfaceCallback(mLifecycle, surfaceCallback));
return null;
- },
- "setSurfaceListener");
+ }
+ );
}
/**
@@ -78,11 +80,11 @@
public void invalidate() {
mHostDispatcher.dispatch(
CarContext.APP_SERVICE,
- (IAppHost host) -> {
+ "invalidate", (IAppHost host) -> {
host.invalidate();
return null;
- },
- "invalidate");
+ }
+ );
}
/**
@@ -97,11 +99,11 @@
requireNonNull(text);
mHostDispatcher.dispatch(
CarContext.APP_SERVICE,
- (IAppHost host) -> {
+ "showToast", (IAppHost host) -> {
host.showToast(text, duration);
return null;
- },
- "showToast");
+ }
+ );
}
/** Returns the {@code IAppManager.Stub} binder. */
@@ -111,11 +113,12 @@
/** Creates an instance of {@link AppManager}. */
static AppManager create(@NonNull CarContext carContext,
- @NonNull HostDispatcher hostDispatcher) {
+ @NonNull HostDispatcher hostDispatcher, @NonNull Lifecycle lifecycle) {
requireNonNull(carContext);
requireNonNull(hostDispatcher);
+ requireNonNull(lifecycle);
- return new AppManager(carContext, hostDispatcher);
+ return new AppManager(carContext, hostDispatcher, lifecycle);
}
// Strictly to avoid synthetic accessor.
@@ -124,38 +127,34 @@
return mCarContext;
}
+ @NonNull
+ Lifecycle getLifecycle() {
+ return mLifecycle;
+ }
+
/** @hide */
@RestrictTo(LIBRARY_GROUP) // Restrict to testing library
- protected AppManager(@NonNull CarContext carContext, @NonNull HostDispatcher hostDispatcher) {
+ protected AppManager(@NonNull CarContext carContext, @NonNull HostDispatcher hostDispatcher,
+ @NonNull Lifecycle lifecycle) {
mCarContext = carContext;
mHostDispatcher = hostDispatcher;
+ mLifecycle = lifecycle;
mAppManager = new IAppManager.Stub() {
@Override
public void getTemplate(IOnDoneCallback callback) {
- ThreadUtils.runOnMain(
- () -> {
- TemplateWrapper templateWrapper;
- try {
- templateWrapper = getCarContext().getCarService(
- ScreenManager.class).getTopTemplate();
- } catch (RuntimeException e) {
- // Catch exceptions, notify the host of it, then rethrow it.
- // This allows the host to log, and show an error to the user.
- RemoteUtils.sendFailureResponse(callback,
- "getTemplate", e);
- throw new RuntimeException(e);
- }
-
- RemoteUtils.sendSuccessResponse(callback, "getTemplate",
- templateWrapper);
- });
+ RemoteUtils.dispatchCallFromHost(getLifecycle(), callback, "getTemplate",
+ getCarContext().getCarService(
+ ScreenManager.class)::getTopTemplate);
}
@Override
public void onBackPressed(IOnDoneCallback callback) {
- RemoteUtils.dispatchHostCall(
- carContext.getOnBackPressedDispatcher()::onBackPressed, callback,
- "onBackPressed");
+ RemoteUtils.dispatchCallFromHost(getLifecycle(), callback,
+ "onBackPressed",
+ () -> {
+ carContext.getOnBackPressedDispatcher().onBackPressed();
+ return null;
+ });
}
};
}
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 84d65e6..5bbb6e0 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
@@ -149,21 +149,17 @@
Log.d(TAG, "onUnbind intent: " + intent);
}
runOnMain(() -> {
- // Destroy the session
if (mCurrentSession != null) {
- CarContext carContext = mCurrentSession.getCarContext();
-
- // Stop any active navigation
- carContext.getCarService(NavigationManager.class).onStopNavigation();
-
- // Destroy all screens in the stack
- carContext.getCarService(ScreenManager.class).destroyAndClearScreenStack();
-
- // Remove binders to the host
- carContext.resetHosts();
-
- ((LifecycleRegistry) mCurrentSession.getLifecycle()).handleLifecycleEvent(
- Event.ON_DESTROY);
+ // Destroy the session
+ // The session's lifecycle is observed by some of the manager and they will
+ // perform cleanup on destroy. For example, the ScreenManager can destroy all
+ // Screens it holds.
+ LifecycleRegistry lifecycleRegistry = getLifecycleIfValid();
+ if (lifecycleRegistry == null) {
+ Log.e(TAG, "Null Session when unbinding");
+ } else {
+ lifecycleRegistry.handleLifecycleEvent(Event.ON_DESTROY);
+ }
}
mCurrentSession = null;
});
@@ -323,6 +319,17 @@
return mHandshakeInfo;
}
+ @Nullable
+ LifecycleRegistry getLifecycleIfValid() {
+ Session session = getCurrentSession();
+ return session == null ? null : (LifecycleRegistry) session.getLifecycle();
+ }
+
+ @NonNull
+ LifecycleRegistry getLifecycle() {
+ return requireNonNull(getLifecycleIfValid());
+ }
+
private final ICarApp.Stub mBinder =
new ICarApp.Stub() {
// incompatible argument for parameter context of attachBaseContext.
@@ -340,10 +347,11 @@
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onAppCreate intent: " + intent);
}
- RemoteUtils.dispatchHostCall(() -> {
+
+ RemoteUtils.dispatchCallFromHost(callback, "onAppCreate", () -> {
Session session = getCurrentSession();
if (session == null
- || session.getLifecycle().getCurrentState() == State.DESTROYED) {
+ || getLifecycle().getCurrentState() == State.DESTROYED) {
session = onCreateSession();
setCurrentSession(session);
}
@@ -354,7 +362,7 @@
// 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) session.getLifecycle();
+ LifecycleRegistry registry = getLifecycle();
Lifecycle.State state = registry.getCurrentState();
int screenStackSize = session.getCarContext().getCarService(
ScreenManager.class).getScreenStack().size();
@@ -374,7 +382,8 @@
}
onNewIntentInternal(session, intent);
}
- }, callback, "onAppCreate");
+ return null;
+ });
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onAppCreate completed");
@@ -383,99 +392,108 @@
@Override
public void onAppStart(IOnDoneCallback callback) {
- RemoteUtils.dispatchHostCall(
- () -> {
- ((LifecycleRegistry) throwIfInvalid(
- getCurrentSession()).getLifecycle())
- .handleLifecycleEvent(Event.ON_START);
- }, callback,
- "onAppStart");
+ RemoteUtils.dispatchCallFromHost(
+ getLifecycleIfValid(), callback,
+ "onAppStart", () -> {
+ getLifecycle().handleLifecycleEvent(Event.ON_START);
+ return null;
+ });
}
@Override
public void onAppResume(IOnDoneCallback callback) {
- RemoteUtils.dispatchHostCall(
- () -> {
- ((LifecycleRegistry) throwIfInvalid(
- getCurrentSession()).getLifecycle())
+ RemoteUtils.dispatchCallFromHost(
+ getLifecycleIfValid(), callback,
+ "onAppResume", () -> {
+ getLifecycle()
.handleLifecycleEvent(Event.ON_RESUME);
- }, callback,
- "onAppResume");
+ return null;
+ });
}
@Override
public void onAppPause(IOnDoneCallback callback) {
- RemoteUtils.dispatchHostCall(
+ RemoteUtils.dispatchCallFromHost(
+ getLifecycleIfValid(), callback, "onAppPause",
() -> {
- ((LifecycleRegistry) throwIfInvalid(
- getCurrentSession()).getLifecycle())
- .handleLifecycleEvent(Event.ON_PAUSE);
- }, callback, "onAppPause");
+ getLifecycle().handleLifecycleEvent(Event.ON_PAUSE);
+ return null;
+ });
}
@Override
public void onAppStop(IOnDoneCallback callback) {
- RemoteUtils.dispatchHostCall(
+ RemoteUtils.dispatchCallFromHost(
+ getLifecycleIfValid(), callback, "onAppStop",
() -> {
- ((LifecycleRegistry) throwIfInvalid(
- getCurrentSession()).getLifecycle())
- .handleLifecycleEvent(Event.ON_STOP);
- }, callback, "onAppStop");
+ getLifecycle().handleLifecycleEvent(Event.ON_STOP);
+ return null;
+ });
}
@Override
public void onNewIntent(Intent intent, IOnDoneCallback callback) {
- RemoteUtils.dispatchHostCall(
- () -> onNewIntentInternal(throwIfInvalid(getCurrentSession()), intent),
+ RemoteUtils.dispatchCallFromHost(
+ getLifecycleIfValid(),
callback,
- "onNewIntent");
+ "onNewIntent",
+ () -> {
+ onNewIntentInternal(throwIfInvalid(getCurrentSession()), intent);
+ return null;
+ });
}
@Override
public void onConfigurationChanged(Configuration configuration,
IOnDoneCallback callback) {
- RemoteUtils.dispatchHostCall(
- () -> onConfigurationChangedInternal(
- throwIfInvalid(getCurrentSession()), configuration),
+ RemoteUtils.dispatchCallFromHost(
+ getLifecycleIfValid(),
callback,
- "onConfigurationChanged");
+ "onConfigurationChanged",
+ () -> {
+ onConfigurationChangedInternal(
+ throwIfInvalid(getCurrentSession()), configuration);
+ return null;
+ });
}
@Override
public void getManager(@CarServiceType @NonNull String type,
IOnDoneCallback callback) {
- Session session = throwIfInvalid(getCurrentSession());
- switch (type) {
- case CarContext.APP_SERVICE:
- RemoteUtils.sendSuccessResponse(
- callback,
- "getManager",
- session.getCarContext().getCarService(
- AppManager.class).getIInterface());
- return;
- case CarContext.NAVIGATION_SERVICE:
- RemoteUtils.sendSuccessResponse(
- callback,
- "getManager",
- session.getCarContext().getCarService(
- NavigationManager.class).getIInterface());
- return;
- default:
- Log.e(TAG, type + "%s is not a valid manager");
- RemoteUtils.sendFailureResponse(callback, "getManager",
- new InvalidParameterException(
- type + " is not a valid manager type"));
- }
+ ThreadUtils.runOnMain(() -> {
+ Session session = throwIfInvalid(getCurrentSession());
+ switch (type) {
+ case CarContext.APP_SERVICE:
+ RemoteUtils.sendSuccessResponseToHost(
+ callback,
+ "getManager",
+ session.getCarContext().getCarService(
+ AppManager.class).getIInterface());
+ return;
+ case CarContext.NAVIGATION_SERVICE:
+ RemoteUtils.sendSuccessResponseToHost(
+ callback,
+ "getManager",
+ session.getCarContext().getCarService(
+ NavigationManager.class).getIInterface());
+ return;
+ default:
+ Log.e(TAG, type + "%s is not a valid manager");
+ RemoteUtils.sendFailureResponseToHost(callback, "getManager",
+ new InvalidParameterException(
+ type + " is not a valid manager type"));
+ }
+ });
}
@Override
public void getAppInfo(IOnDoneCallback callback) {
try {
- RemoteUtils.sendSuccessResponse(
+ RemoteUtils.sendSuccessResponseToHost(
callback, "getAppInfo", CarAppService.this.getAppInfo());
} catch (IllegalArgumentException e) {
// getAppInfo() could fail with the specified API version is invalid.
- RemoteUtils.sendFailureResponse(callback, "getAppInfo", e);
+ RemoteUtils.sendFailureResponseToHost(callback, "getAppInfo", e);
}
}
@@ -489,17 +507,18 @@
int uid = Binder.getCallingUid();
HostInfo hostInfo = new HostInfo(packageName, uid);
if (!getHostValidator().isValidHost(hostInfo)) {
- RemoteUtils.sendFailureResponse(callback, "onHandshakeCompleted",
+ RemoteUtils.sendFailureResponseToHost(callback, "onHandshakeCompleted",
new IllegalArgumentException("Unknown host '"
+ packageName + "', uid:" + uid));
return;
}
setHostInfo(hostInfo);
setHandshakeInfo(deserializedHandshakeInfo);
- RemoteUtils.sendSuccessResponse(callback, "onHandshakeCompleted", null);
+ RemoteUtils.sendSuccessResponseToHost(callback, "onHandshakeCompleted",
+ null);
} catch (BundlerException | IllegalArgumentException e) {
setHostInfo(null);
- RemoteUtils.sendFailureResponse(callback, "onHandshakeCompleted", e);
+ RemoteUtils.sendFailureResponseToHost(callback, "onHandshakeCompleted", e);
}
}
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 f9fb37a..1589314 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
@@ -46,9 +46,13 @@
import androidx.car.app.utils.ThreadUtils;
import androidx.car.app.versioning.CarAppApiLevel;
import androidx.car.app.versioning.CarAppApiLevels;
+import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
+import org.jetbrains.annotations.NotNull;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.security.InvalidParameterException;
@@ -256,11 +260,11 @@
mHostDispatcher.dispatch(
CarContext.CAR_SERVICE,
- (ICarHost host) -> {
+ "startCarApp", (ICarHost host) -> {
host.startCarApp(intent);
return null;
- },
- "startCarApp");
+ }
+ );
}
/**
@@ -294,12 +298,12 @@
IStartCarApp startCarAppInterface = requireNonNull(IStartCarApp.Stub.asInterface(binder));
- RemoteUtils.call(
- () -> {
+ RemoteUtils.dispatchCallToHost(
+ "startCarApp from notification", () -> {
startCarAppInterface.startCarApp(appIntent);
return null;
- },
- "startCarApp from notification");
+ }
+ );
}
/**
@@ -313,11 +317,11 @@
public void finishCarApp() {
mHostDispatcher.dispatch(
CarContext.CAR_SERVICE,
- (ICarHost host) -> {
+ "finish", (ICarHost host) -> {
host.finish();
return null;
- },
- "finish");
+ }
+ );
}
/**
@@ -440,14 +444,6 @@
mHostDispatcher.setCarHost(requireNonNull(carHost));
}
- /** @hide */
- @RestrictTo(LIBRARY_GROUP) // Restrict to testing library
- @MainThread
- void resetHosts() {
- ThreadUtils.checkMainThread();
- mHostDispatcher.resetHosts();
- }
-
/**
* Retrieves the API level negotiated with the host.
*
@@ -491,10 +487,20 @@
super(null);
mHostDispatcher = hostDispatcher;
- mAppManager = AppManager.create(this, hostDispatcher);
- mNavigationManager = NavigationManager.create(this, hostDispatcher);
+ mAppManager = AppManager.create(this, hostDispatcher, lifecycle);
+ mNavigationManager = NavigationManager.create(this, hostDispatcher, lifecycle);
mScreenManager = ScreenManager.create(this, lifecycle);
mOnBackPressedDispatcher =
new OnBackPressedDispatcher(() -> getCarService(ScreenManager.class).pop());
+
+ LifecycleObserver observer = new DefaultLifecycleObserver() {
+ @Override
+ public void onDestroy(@NonNull @NotNull LifecycleOwner owner) {
+ hostDispatcher.resetHosts();
+ owner.getLifecycle().removeObserver(this);
+ }
+ };
+
+ lifecycle.addObserver(observer);
}
}
diff --git a/car/app/app/src/main/java/androidx/car/app/HostDispatcher.java b/car/app/app/src/main/java/androidx/car/app/HostDispatcher.java
index ff206e3..dc21714 100644
--- a/car/app/app/src/main/java/androidx/car/app/HostDispatcher.java
+++ b/car/app/app/src/main/java/androidx/car/app/HostDispatcher.java
@@ -21,8 +21,9 @@
import static java.util.Objects.requireNonNull;
-import android.annotation.SuppressLint;
import android.os.IInterface;
+import android.os.RemoteException;
+import android.util.Log;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
@@ -30,6 +31,7 @@
import androidx.annotation.RestrictTo;
import androidx.car.app.CarContext.CarServiceType;
import androidx.car.app.navigation.INavigationHost;
+import androidx.car.app.utils.LogTags;
import androidx.car.app.utils.RemoteUtils;
import androidx.car.app.utils.ThreadUtils;
@@ -53,19 +55,55 @@
* Dispatches the {@code call} to the host for the given {@code hostType}.
*
* @param hostType the service to dispatch to
- * @param call the request to dispatch
* @param callName the name of the call for logging purposes
+ * @param call the request to dispatch
+ *
+ * @throws RemoteException if the host is unresponsive
* @throws SecurityException if the host has thrown it
* @throws HostException if the host throws any exception other than
* {@link SecurityException}
*/
@Nullable
@SuppressWarnings({"unchecked", "cast.unsafe"}) // Cannot check if instanceof ServiceT
- @SuppressLint("LambdaLast")
- public <ServiceT, ReturnT> ReturnT dispatch(
- @CarServiceType @NonNull String hostType, @NonNull HostCall<ServiceT, ReturnT> call,
- @NonNull String callName) {
- return RemoteUtils.call(() -> call.dispatch((ServiceT) getHost(hostType)), callName);
+ public <ServiceT, ReturnT> ReturnT dispatchForResult(
+ @CarServiceType @NonNull String hostType, @NonNull String callName,
+ @NonNull HostCall<ServiceT, ReturnT> call) throws RemoteException {
+ return RemoteUtils.dispatchCallToHostForResult(callName, () -> {
+ IInterface service = getHost(hostType);
+ if (service == null) {
+ Log.e(LogTags.TAG_DISPATCH,
+ "Could not retrieve host while dispatching call " + callName);
+ return null;
+ }
+ return call.dispatch((ServiceT) service);
+ });
+ }
+
+ /**
+ * Dispatches the {@code call} to the host for the given {@code hostType}.
+ *
+ * @param hostType the service to dispatch to
+ * @param callName the name of the call for logging purposes
+ * @param call the request to dispatch
+ *
+ * @throws SecurityException if the host has thrown it
+ * @throws HostException if the host throws any exception other than
+ * {@link SecurityException}
+ */
+ @SuppressWarnings({"unchecked", "cast.unsafe"}) // Cannot check if instanceof ServiceT
+ public <ServiceT, ReturnT> void dispatch(
+ @CarServiceType @NonNull String hostType, @NonNull String callName,
+ @NonNull HostCall<ServiceT, ReturnT> call) {
+ RemoteUtils.dispatchCallToHost(callName, () -> {
+ IInterface service = getHost(hostType);
+ if (service == null) {
+ Log.e(LogTags.TAG_DISPATCH,
+ "Could not retrieve host while dispatching call " + callName);
+ return null;
+ }
+ call.dispatch((ServiceT) service);
+ return null;
+ });
}
@MainThread
@@ -89,12 +127,16 @@
/**
* Retrieves the {@link IInterface} for the given {@code hostType}.
*
+ * @throws RemoteException if the host is unresponsive
* @hide
*/
@RestrictTo(LIBRARY)
- IInterface getHost(@CarServiceType String hostType) {
+ @Nullable
+ IInterface getHost(@CarServiceType String hostType) throws RemoteException {
if (mCarHost == null) {
- throw new HostException("Host is not bound when attempting to retrieve host service");
+ Log.e(LogTags.TAG_DISPATCH, "Host is not bound when attempting to retrieve host "
+ + "service");
+ return null;
}
IInterface host = null;
@@ -102,21 +144,21 @@
case CarContext.APP_SERVICE:
if (mAppHost == null) {
mAppHost =
- RemoteUtils.call(() ->
+ RemoteUtils.dispatchCallToHostForResult("getHost(App)", () ->
IAppHost.Stub.asInterface(requireNonNull(mCarHost).getHost(
- CarContext.APP_SERVICE)), "getHost(App)");
+ CarContext.APP_SERVICE)));
}
host = mAppHost;
break;
case CarContext.NAVIGATION_SERVICE:
if (mNavigationHost == null) {
mNavigationHost =
- RemoteUtils.call(
- () ->
+ RemoteUtils.dispatchCallToHostForResult(
+ "getHost(Navigation)", () ->
INavigationHost.Stub.asInterface(
requireNonNull(mCarHost).getHost(
- CarContext.NAVIGATION_SERVICE)),
- "getHost(Navigation)");
+ CarContext.NAVIGATION_SERVICE))
+ );
}
host = mNavigationHost;
break;
@@ -126,6 +168,6 @@
default:
throw new InvalidParameterException("Invalid host type: " + hostType);
}
- return requireNonNull(host);
+ return 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 38d3707..d274a66 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
@@ -406,6 +406,7 @@
@Override
public void onDestroy(@NonNull LifecycleOwner lifecycleOwner) {
destroyAndClearScreenStack();
+ lifecycleOwner.getLifecycle().removeObserver(this);
}
}
}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/OnCheckedChangeDelegateImpl.java b/car/app/app/src/main/java/androidx/car/app/model/OnCheckedChangeDelegateImpl.java
index 9b4cf8c..202251e 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/OnCheckedChangeDelegateImpl.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnCheckedChangeDelegateImpl.java
@@ -80,9 +80,11 @@
@Override
public void onCheckedChange(boolean isChecked, IOnDoneCallback callback) {
- RemoteUtils.dispatchHostCall(
- () -> mListener.onCheckedChange(isChecked), callback,
- "onCheckedChange");
+ RemoteUtils.dispatchCallFromHost(callback, "onCheckedChange", () -> {
+ mListener.onCheckedChange(isChecked);
+ return null;
+ }
+ );
}
}
}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/OnClickDelegateImpl.java b/car/app/app/src/main/java/androidx/car/app/model/OnClickDelegateImpl.java
index 79612f6..a786696 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/OnClickDelegateImpl.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnClickDelegateImpl.java
@@ -93,7 +93,10 @@
@Override
public void onClick(IOnDoneCallback callback) {
- RemoteUtils.dispatchHostCall(mOnClickListener::onClick, callback, "onClick");
+ RemoteUtils.dispatchCallFromHost(callback, "onClick", () -> {
+ mOnClickListener.onClick();
+ return null;
+ });
}
}
}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/OnItemVisibilityChangedDelegateImpl.java b/car/app/app/src/main/java/androidx/car/app/model/OnItemVisibilityChangedDelegateImpl.java
index b6b515e..7e7bdcdb 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/OnItemVisibilityChangedDelegateImpl.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnItemVisibilityChangedDelegateImpl.java
@@ -88,11 +88,13 @@
@Override
public void onItemVisibilityChanged(
int startIndexInclusive, int endIndexExclusive, IOnDoneCallback callback) {
- RemoteUtils.dispatchHostCall(
- () -> mListener.onItemVisibilityChanged(
- startIndexInclusive, endIndexExclusive),
- callback,
- "onItemVisibilityChanged");
+ RemoteUtils.dispatchCallFromHost(
+ callback, "onItemVisibilityChanged", () -> {
+ mListener.onItemVisibilityChanged(
+ startIndexInclusive, endIndexExclusive);
+ return null;
+ }
+ );
}
}
}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/OnSelectedDelegateImpl.java b/car/app/app/src/main/java/androidx/car/app/model/OnSelectedDelegateImpl.java
index 5297c63..f1e7414 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/OnSelectedDelegateImpl.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnSelectedDelegateImpl.java
@@ -80,8 +80,11 @@
@Override
public void onSelected(int index, IOnDoneCallback callback) {
- RemoteUtils.dispatchHostCall(
- () -> mListener.onSelected(index), callback, "onSelectedListener");
+ RemoteUtils.dispatchCallFromHost(
+ callback, "onSelectedListener", () -> {
+ mListener.onSelected(index);
+ return null;
+ });
}
}
}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/SearchCallbackDelegateImpl.java b/car/app/app/src/main/java/androidx/car/app/model/SearchCallbackDelegateImpl.java
index 0db19ad..abfbf5d 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/SearchCallbackDelegateImpl.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/SearchCallbackDelegateImpl.java
@@ -92,15 +92,21 @@
@Override
public void onSearchTextChanged(String text, IOnDoneCallback callback) {
- RemoteUtils.dispatchHostCall(
- () -> mCallback.onSearchTextChanged(text), callback,
- "onSearchTextChanged");
+ RemoteUtils.dispatchCallFromHost(
+ callback, "onSearchTextChanged", () -> {
+ mCallback.onSearchTextChanged(text);
+ return null;
+ }
+ );
}
@Override
public void onSearchSubmitted(String text, IOnDoneCallback callback) {
- RemoteUtils.dispatchHostCall(
- () -> mCallback.onSearchSubmitted(text), callback, "onSearchSubmitted");
+ RemoteUtils.dispatchCallFromHost(
+ callback, "onSearchSubmitted", () -> {
+ mCallback.onSearchSubmitted(text);
+ return null;
+ });
}
}
}
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 6f22d72..c181f0d 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
@@ -41,6 +41,10 @@
import androidx.car.app.serialization.BundlerException;
import androidx.car.app.utils.RemoteUtils;
import androidx.core.content.ContextCompat;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
import java.util.concurrent.Executor;
@@ -117,11 +121,11 @@
mHostDispatcher.dispatch(
CarContext.NAVIGATION_SERVICE,
- (INavigationHost service) -> {
+ "updateTrip", (INavigationHost service) -> {
service.updateTrip(bundle);
return null;
- },
- "updateTrip");
+ }
+ );
}
/**
@@ -207,11 +211,11 @@
mIsNavigating = true;
mHostDispatcher.dispatch(
CarContext.NAVIGATION_SERVICE,
- (INavigationHost service) -> {
+ "navigationStarted", (INavigationHost service) -> {
service.navigationStarted();
return null;
- },
- "navigationStarted");
+ }
+ );
}
/**
@@ -234,11 +238,11 @@
mIsNavigating = false;
mHostDispatcher.dispatch(
CarContext.NAVIGATION_SERVICE,
- (INavigationHost service) -> {
+ "navigationEnded", (INavigationHost service) -> {
service.navigationEnded();
return null;
- },
- "navigationEnded");
+ }
+ );
}
/**
@@ -249,8 +253,12 @@
@RestrictTo(LIBRARY)
@NonNull
public static NavigationManager create(@NonNull CarContext carContext,
- @NonNull HostDispatcher hostDispatcher) {
- return new NavigationManager(carContext, hostDispatcher);
+ @NonNull HostDispatcher hostDispatcher, @NonNull Lifecycle lifecycle) {
+ requireNonNull(carContext);
+ requireNonNull(hostDispatcher);
+ requireNonNull(lifecycle);
+
+ return new NavigationManager(carContext, hostDispatcher, lifecycle);
}
/**
@@ -277,9 +285,14 @@
return;
}
mIsNavigating = false;
- requireNonNull(mNavigationManagerCallbackExecutor).execute(() -> {
- requireNonNull(mNavigationManagerCallback).onStopNavigation();
- });
+
+ NavigationManagerCallback callback = mNavigationManagerCallback;
+ Executor executor = mNavigationManagerCallbackExecutor;
+ if (callback == null || executor == null) {
+ return;
+ }
+
+ executor.execute(callback::onStopNavigation);
}
/**
@@ -300,32 +313,45 @@
}
mIsAutoDriveEnabled = true;
+
NavigationManagerCallback callback = mNavigationManagerCallback;
- if (callback != null) {
- requireNonNull(mNavigationManagerCallbackExecutor).execute(() -> {
- callback.onAutoDriveEnabled();
- });
- } else {
+ Executor executor = mNavigationManagerCallbackExecutor;
+ if (callback == null || executor == null) {
Log.w(TAG_NAVIGATION_MANAGER,
"NavigationManagerCallback not set, skipping onAutoDriveEnabled");
+ return;
}
+
+ executor.execute(callback::onAutoDriveEnabled);
}
/** @hide */
@RestrictTo(LIBRARY_GROUP) // Restrict to testing library
@SuppressWarnings({"methodref.receiver.bound.invalid"})
protected NavigationManager(@NonNull CarContext carContext,
- @NonNull HostDispatcher hostDispatcher) {
- mCarContext = carContext;
+ @NonNull HostDispatcher hostDispatcher, @NonNull Lifecycle lifecycle) {
+ mCarContext = requireNonNull(carContext);
mHostDispatcher = requireNonNull(hostDispatcher);
mNavigationManager =
new INavigationManager.Stub() {
@Override
public void onStopNavigation(IOnDoneCallback callback) {
- RemoteUtils.dispatchHostCall(
- NavigationManager.this::onStopNavigation, callback,
- "onStopNavigation");
+ RemoteUtils.dispatchCallFromHost(
+ lifecycle, callback,
+ "onStopNavigation",
+ () -> {
+ NavigationManager.this.onStopNavigation();
+ return null;
+ });
}
};
+ LifecycleObserver observer = new DefaultLifecycleObserver() {
+ @Override
+ public void onDestroy(@NonNull LifecycleOwner lifecycleOwner) {
+ NavigationManager.this.onStopNavigation();
+ lifecycle.removeObserver(this);
+ }
+ };
+ lifecycle.addObserver(observer);
}
}
diff --git a/car/app/app/src/main/java/androidx/car/app/utils/LogTags.java b/car/app/app/src/main/java/androidx/car/app/utils/LogTags.java
index cb162f5..e831f1d 100644
--- a/car/app/app/src/main/java/androidx/car/app/utils/LogTags.java
+++ b/car/app/app/src/main/java/androidx/car/app/utils/LogTags.java
@@ -30,7 +30,10 @@
/** Tag to use for logging in the library. */
public static final String TAG = "CarApp";
- /** Tag to use for host validation. */
+ /** Tag to use for IPC dispatching */
+ public static final String TAG_DISPATCH = TAG + ".Dispatch";
+
+ /** Tag to use for host validation */
public static final String TAG_HOST_VALIDATION = TAG + ".Val";
/** Tag to use for navigation manager. */
diff --git a/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java b/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java
index 2567d7c..9b6ce95 100644
--- a/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java
+++ b/car/app/app/src/main/java/androidx/car/app/utils/RemoteUtils.java
@@ -18,8 +18,8 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.car.app.utils.LogTags.TAG;
+import static androidx.lifecycle.Lifecycle.State.CREATED;
-import android.annotation.SuppressLint;
import android.graphics.Rect;
import android.os.RemoteException;
import android.util.Log;
@@ -36,6 +36,7 @@
import androidx.car.app.SurfaceContainer;
import androidx.car.app.serialization.Bundleable;
import androidx.car.app.serialization.BundlerException;
+import androidx.lifecycle.Lifecycle;
/**
* Assorted utilities to deal with serialization of remote calls.
@@ -56,19 +57,28 @@
* success/failure.
*/
public interface HostCall {
- void dispatch() throws BundlerException;
+ /**
+ * Dispatches the call and returns its outcome if any.
+ *
+ * @return the response from the app for the host call, or {@code null} if there is
+ * nothing to return
+ */
+ @Nullable
+ Object dispatch() throws BundlerException;
}
/**
- * Performs the remote call and handles exceptions thrown by the host.
+ * Performs the remote call to the host and handles exceptions thrown by the host.
*
+ * @return the value that the host returns for the IPC
+ *
+ * @throws RemoteException if the host is unresponsive
* @throws SecurityException as a pass through from the host
* @throws HostException if the remote call fails with any other exception
*/
- @SuppressLint("LambdaLast")
@Nullable
- public static <ReturnT> ReturnT call(@NonNull RemoteCall<ReturnT> remoteCall,
- @NonNull String callName) {
+ public static <ReturnT> ReturnT dispatchCallToHostForResult(@NonNull String callName,
+ @NonNull RemoteCall<ReturnT> remoteCall) throws RemoteException {
try {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Dispatching call " + callName + " to host");
@@ -78,23 +88,44 @@
// SecurityException is treated specially where we allow it to flow through since
// this is specific to not having permissions to perform an API.
throw e;
- } catch (RemoteException | RuntimeException e) {
+ } catch (RuntimeException e) {
throw new HostException("Remote " + callName + " call failed", e);
}
}
/**
+ * Performs the remote call to the host and handles exceptions thrown by the host.
+ *
+ * @throws SecurityException as a pass through from the host
+ * @throws HostException if the remote call fails with any other exception
+ */
+ public static void dispatchCallToHost(@NonNull String callName,
+ @NonNull RemoteCall<?> remoteCall) {
+ try {
+ dispatchCallToHostForResult(callName, remoteCall);
+ } catch (RemoteException e) {
+ // The host is dead, don't crash the app, just log.
+ Log.e(LogTags.TAG_DISPATCH, "Host unresponsive when dispatching call " + callName, e);
+ }
+ }
+
+ /**
* Returns an {@link ISurfaceCallback} stub that invokes the input {@link SurfaceCallback}
* if it is not {@code null}, or {@code null} if the input {@link SurfaceCallback} is {@code
* null}
+ *
+ * @param lifecycle the lifecycle of the session to be used to not dispatch calls out of
+ * lifecycle.
+ * @param surfaceCallback the callback to wrap in an {@link ISurfaceCallback}
*/
@Nullable
- public static ISurfaceCallback stubSurfaceCallback(@Nullable SurfaceCallback surfaceCallback) {
+ public static ISurfaceCallback stubSurfaceCallback(@NonNull Lifecycle lifecycle,
+ @Nullable SurfaceCallback surfaceCallback) {
if (surfaceCallback == null) {
return null;
}
- return new SurfaceCallbackStub(surfaceCallback);
+ return new SurfaceCallbackStub(lifecycle, surfaceCallback);
}
/**
@@ -107,22 +138,51 @@
* <p>If the app throws an exception, will call {@link IOnDoneCallback#onFailure} with a {@link
* FailureResponse} including information from the caught exception.
*/
- @SuppressLint("LambdaLast")
- public static void dispatchHostCall(
- @NonNull HostCall hostCall, @NonNull IOnDoneCallback callback,
- @NonNull String callName) {
+ public static void dispatchCallFromHost(
+ @NonNull IOnDoneCallback callback, @NonNull String callName,
+ @NonNull HostCall hostCall) {
+ // TODO(b/180530156): Move callers that should be lifecycle aware once we can put a
+ // lifecycle into a Template and propagate it to the models.
ThreadUtils.runOnMain(
() -> {
try {
- hostCall.dispatch();
- } catch (BundlerException e) {
- sendFailureResponse(callback, callName, e);
- throw new HostException("Serialization failure in " + callName, e);
+ sendSuccessResponseToHost(callback, callName, hostCall.dispatch());
} catch (RuntimeException e) {
- sendFailureResponse(callback, callName, e);
+ // Catch exceptions, notify the host of it, then rethrow it.
+ // This allows the host to log, and show an error to the user.
+ sendFailureResponseToHost(callback, callName, e);
throw new RuntimeException(e);
+ } catch (BundlerException e) {
+ sendFailureResponseToHost(callback, callName, e);
}
- sendSuccessResponse(callback, callName, null);
+ });
+ }
+
+ /**
+ * Dispatches the given {@link HostCall} to the client in the main thread, but only if the
+ * provided {@link Lifecycle} has a state of at least created, and notifies the host of outcome.
+ *
+ * <p>If the app processes the response, will call {@link IOnDoneCallback#onSuccess} with a
+ * {@code null}.
+ *
+ * <p>If the app throws an exception, will call {@link IOnDoneCallback#onFailure} with a {@link
+ * FailureResponse} including information from the caught exception.
+ *
+ * <p>If the {@code lifecycle} provided is {@code null} or not at least created, will call
+ * {@link IOnDoneCallback#onFailure} with a {@link FailureResponse}.
+ */
+ public static void dispatchCallFromHost(
+ @Nullable Lifecycle lifecycle, @NonNull IOnDoneCallback callback,
+ @NonNull String callName, @NonNull HostCall hostCall) {
+ ThreadUtils.runOnMain(
+ () -> {
+ if (lifecycle == null || !lifecycle.getCurrentState().isAtLeast(CREATED)) {
+ sendFailureResponseToHost(callback, callName, new IllegalStateException(
+ "Lifecycle is not at least created when dispatching " + hostCall));
+ return;
+ }
+
+ dispatchCallFromHost(callback, callName, hostCall);
});
}
@@ -131,36 +191,35 @@
*/
// TODO(b/178748627): the nullable annotation from the AIDL file is not being considered.
@SuppressWarnings("NullAway")
- public static void sendSuccessResponse(
+ public static void sendSuccessResponseToHost(
@NonNull IOnDoneCallback callback, @NonNull String callName,
@Nullable Object response) {
- call(() -> {
+ dispatchCallToHost(callName + " onSuccess", () -> {
try {
callback.onSuccess(response == null ? null : Bundleable.create(response));
} catch (BundlerException e) {
- sendFailureResponse(callback, callName, e);
- throw new IllegalStateException("Serialization failure in " + callName, e);
+ sendFailureResponseToHost(callback, callName, e);
}
return null;
- }, callName + " onSuccess");
+ });
}
/**
* Invoke onFailure on the given {@code callback} instance with the given {@link Throwable}.
*/
- public static void sendFailureResponse(@NonNull IOnDoneCallback callback,
+ public static void sendFailureResponseToHost(@NonNull IOnDoneCallback callback,
@NonNull String callName,
@NonNull Throwable e) {
- call(() -> {
+ dispatchCallToHost(callName + " onFailure", () -> {
try {
callback.onFailure(Bundleable.create(new FailureResponse(e)));
} catch (BundlerException bundlerException) {
// Not possible, but catching since BundlerException is not runtime.
- throw new IllegalStateException(
+ Log.e(LogTags.TAG_DISPATCH,
"Serialization failure in " + callName, bundlerException);
}
return null;
- }, callName + " onFailure");
+ });
}
/**
@@ -183,43 +242,60 @@
}
private static class SurfaceCallbackStub extends ISurfaceCallback.Stub {
+ private final Lifecycle mLifecycle;
private final SurfaceCallback mSurfaceCallback;
- SurfaceCallbackStub(SurfaceCallback surfaceCallback) {
+ SurfaceCallbackStub(Lifecycle lifecycle, SurfaceCallback surfaceCallback) {
+ mLifecycle = lifecycle;
mSurfaceCallback = surfaceCallback;
}
@Override
public void onSurfaceAvailable(Bundleable surfaceContainer, IOnDoneCallback callback) {
- dispatchHostCall(
- () -> mSurfaceCallback.onSurfaceAvailable(
- (SurfaceContainer) surfaceContainer.get()),
+ dispatchCallFromHost(
+ mLifecycle,
callback,
- "onSurfaceAvailable");
+ "onSurfaceAvailable",
+ () -> {
+ mSurfaceCallback.onSurfaceAvailable(
+ (SurfaceContainer) surfaceContainer.get());
+ return null;
+ });
}
@Override
public void onVisibleAreaChanged(Rect visibleArea, IOnDoneCallback callback) {
- dispatchHostCall(
- () -> mSurfaceCallback.onVisibleAreaChanged(visibleArea),
+ dispatchCallFromHost(
+ mLifecycle,
callback,
- "onVisibleAreaChanged");
+ "onVisibleAreaChanged",
+ () -> {
+ mSurfaceCallback.onVisibleAreaChanged(visibleArea);
+ return null;
+ });
}
@Override
public void onStableAreaChanged(Rect stableArea, IOnDoneCallback callback) {
- dispatchHostCall(
- () -> mSurfaceCallback.onStableAreaChanged(stableArea), callback,
- "onStableAreaChanged");
+ dispatchCallFromHost(
+ mLifecycle, callback,
+ "onStableAreaChanged", () -> {
+ mSurfaceCallback.onStableAreaChanged(stableArea);
+ return null;
+ });
}
@Override
public void onSurfaceDestroyed(Bundleable surfaceContainer, IOnDoneCallback callback) {
- dispatchHostCall(
- () -> mSurfaceCallback.onSurfaceDestroyed(
- (SurfaceContainer) surfaceContainer.get()),
+ dispatchCallFromHost(
+ mLifecycle,
callback,
- "onSurfaceDestroyed");
+ "onSurfaceDestroyed",
+ () -> {
+ mSurfaceCallback.onSurfaceDestroyed(
+ (SurfaceContainer) surfaceContainer.get());
+ return null;
+ });
}
}
diff --git a/car/app/app/src/test/java/androidx/car/app/AppManagerTest.java b/car/app/app/src/test/java/androidx/car/app/AppManagerTest.java
index 941c03e..5b3d169 100644
--- a/car/app/app/src/test/java/androidx/car/app/AppManagerTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/AppManagerTest.java
@@ -21,20 +21,28 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.graphics.Rect;
import android.os.RemoteException;
+import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.model.Template;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
import androidx.car.app.testing.TestCarContext;
+import androidx.lifecycle.Lifecycle;
import androidx.test.core.app.ApplicationProvider;
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;
@@ -50,6 +58,13 @@
private IAppHost.Stub mMockAppHost;
@Mock
private IOnDoneCallback mMockOnDoneCallback;
+ @Mock
+ private OnBackPressedCallback mMockOnBackPressedCallback;
+ @Mock
+ private SurfaceCallback mSurfaceCallback;
+
+ @Captor
+ private ArgumentCaptor<ISurfaceCallback> mSurfaceCallbackCaptor;
private TestCarContext mTestCarContext;
private final HostDispatcher mHostDispatcher = new HostDispatcher();
@@ -86,30 +101,90 @@
mHostDispatcher.setCarHost(mMockCarHost);
- mAppManager = AppManager.create(mTestCarContext, mHostDispatcher);
+ mAppManager = AppManager.create(mTestCarContext, mHostDispatcher,
+ mTestCarContext.getLifecycleOwner().mRegistry);
}
@Test
- public void getTemplate_serializationFails_throwsIllegalStateException()
- throws RemoteException {
+ public void getTemplate_lifecycleCreated_sendsToApp() throws RemoteException {
+ mTestCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.CREATED);
mTestCarContext
.getCarService(ScreenManager.class)
.push(new Screen(mTestCarContext) {
@NonNull
@Override
public Template onGetTemplate() {
- return new Template() {
+ return new TestTemplate();
+ }
+ });
+
+ mAppManager.getIInterface().getTemplate(mMockOnDoneCallback);
+
+ verify(mMockOnDoneCallback).onSuccess(any());
+ }
+
+ @Test
+ public void getTemplate_lifecycleNotCreated_doesNotSendToApp() throws RemoteException {
+ mTestCarContext
+ .getCarService(ScreenManager.class)
+ .push(new Screen(mTestCarContext) {
+ @NonNull
+ @Override
+ public Template onGetTemplate() {
+ return new TestTemplate() {
};
}
});
- assertThrows(
- HostException.class,
- () -> mAppManager.getIInterface().getTemplate(mMockOnDoneCallback));
+ mAppManager.getIInterface().getTemplate(mMockOnDoneCallback);
+
verify(mMockOnDoneCallback).onFailure(any());
}
@Test
+ public void getTemplate_serializationFails_sendsFailureToHost()
+ throws RemoteException {
+ mTestCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.CREATED);
+ mTestCarContext
+ .getCarService(ScreenManager.class)
+ .push(new Screen(mTestCarContext) {
+ @NonNull
+ @Override
+ public Template onGetTemplate() {
+ return new NonBundleableTemplate("foo") {
+ };
+ }
+ });
+
+ mAppManager.getIInterface().getTemplate(mMockOnDoneCallback);
+
+ verify(mMockOnDoneCallback).onFailure(any());
+ }
+
+ @Test
+ public void onBackPressed_lifecycleCreated_sendsToApp() throws RemoteException {
+ mTestCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.CREATED);
+ when(mMockOnBackPressedCallback.isEnabled()).thenReturn(true);
+ mTestCarContext.getOnBackPressedDispatcher().addCallback(mMockOnBackPressedCallback);
+
+ mAppManager.getIInterface().onBackPressed(mMockOnDoneCallback);
+
+ verify(mMockOnDoneCallback).onSuccess(any());
+ verify(mMockOnBackPressedCallback).handleOnBackPressed();
+ }
+
+ @Test
+ public void onBackPressed_lifecycleNotCreated_doesNotSendToApp() throws RemoteException {
+ when(mMockOnBackPressedCallback.isEnabled()).thenReturn(true);
+ mTestCarContext.getOnBackPressedDispatcher().addCallback(mMockOnBackPressedCallback);
+
+ mAppManager.getIInterface().onBackPressed(mMockOnDoneCallback);
+
+ verify(mMockOnDoneCallback).onFailure(any());
+ verify(mMockOnBackPressedCallback, never()).handleOnBackPressed();
+ }
+
+ @Test
public void invalidate_forwardsRequestToHost() throws RemoteException {
mAppManager.invalidate();
@@ -117,11 +192,11 @@
}
@Test
- public void invalidate_hostThrowsRemoteException_throwsHostException() throws
+ public void invalidate_hostThrowsRemoteException_doesNotThrow() throws
RemoteException {
doThrow(new RemoteException()).when(mMockAppHost).invalidate();
- assertThrows(HostException.class, () -> mAppManager.invalidate());
+ mAppManager.invalidate();
}
@Test
@@ -142,10 +217,10 @@
}
@Test
- public void showToast_hostThrowsRemoteException_throwsHostException() throws RemoteException {
+ public void showToast_hostThrowsRemoteException_doesNotThrow() throws RemoteException {
doThrow(new RemoteException()).when(mMockAppHost).showToast(anyString(), anyInt());
- assertThrows(HostException.class, () -> mAppManager.showToast("foo", 1));
+ mAppManager.showToast("foo", 1);
}
@Test
@@ -172,11 +247,11 @@
}
@Test
- public void etSurfaceListener_hostThrowsRemoteException_throwsHostException()
+ public void etSurfaceListener_hostThrowsRemoteException_doesNotThrow()
throws RemoteException {
doThrow(new RemoteException()).when(mMockAppHost).setSurfaceCallback(any());
- assertThrows(HostException.class, () -> mAppManager.setSurfaceCallback(null));
+ mAppManager.setSurfaceCallback(null);
}
@Test
@@ -186,4 +261,124 @@
assertThrows(HostException.class, () -> mAppManager.setSurfaceCallback(null));
}
+
+ @Test
+ public void onSurfaceAvailable_dispatches()
+ throws RemoteException, BundlerException {
+ mTestCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.CREATED);
+ mAppManager.setSurfaceCallback(mSurfaceCallback);
+ verify(mMockAppHost).setSurfaceCallback(mSurfaceCallbackCaptor.capture());
+ SurfaceContainer surfaceContainer = new SurfaceContainer(null, 1, 2, 3);
+
+ mSurfaceCallbackCaptor.getValue().onSurfaceAvailable(Bundleable.create(surfaceContainer),
+ mMockOnDoneCallback);
+
+ verify(mSurfaceCallback).onSurfaceAvailable(any());
+ verify(mMockOnDoneCallback).onSuccess(any());
+ }
+
+ @Test
+ public void onSurfaceAvailable_lifecycleNotCreated_doesNotDispatch_sendsAFailure()
+ throws RemoteException, BundlerException {
+ mAppManager.setSurfaceCallback(mSurfaceCallback);
+ verify(mMockAppHost).setSurfaceCallback(mSurfaceCallbackCaptor.capture());
+ SurfaceContainer surfaceContainer = new SurfaceContainer(null, 1, 2, 3);
+
+ mSurfaceCallbackCaptor.getValue().onSurfaceAvailable(Bundleable.create(surfaceContainer),
+ mMockOnDoneCallback);
+
+ verify(mSurfaceCallback, never()).onSurfaceAvailable(surfaceContainer);
+ verify(mMockOnDoneCallback).onFailure(any());
+ }
+
+ @Test
+ public void onVisibleAreaChanged_dispatches()
+ throws RemoteException {
+ mTestCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.CREATED);
+ mAppManager.setSurfaceCallback(mSurfaceCallback);
+ verify(mMockAppHost).setSurfaceCallback(mSurfaceCallbackCaptor.capture());
+ Rect rect = new Rect(0, 0, 1, 1);
+
+ mSurfaceCallbackCaptor.getValue().onVisibleAreaChanged(rect, mMockOnDoneCallback);
+
+ verify(mSurfaceCallback).onVisibleAreaChanged(rect);
+ verify(mMockOnDoneCallback).onSuccess(any());
+ }
+
+ @Test
+ public void onVisibleAreaChanged_lifecycleNotCreated_doesNotDispatch_sendsAFailure()
+ throws RemoteException {
+ mAppManager.setSurfaceCallback(mSurfaceCallback);
+ verify(mMockAppHost).setSurfaceCallback(mSurfaceCallbackCaptor.capture());
+ Rect rect = new Rect(0, 0, 1, 1);
+
+ mSurfaceCallbackCaptor.getValue().onVisibleAreaChanged(rect, mMockOnDoneCallback);
+
+ verify(mSurfaceCallback, never()).onVisibleAreaChanged(any());
+ verify(mMockOnDoneCallback).onFailure(any());
+ }
+
+ @Test
+ public void onStableAreaChanged_dispatches()
+ throws RemoteException {
+ mTestCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.CREATED);
+ mAppManager.setSurfaceCallback(mSurfaceCallback);
+ verify(mMockAppHost).setSurfaceCallback(mSurfaceCallbackCaptor.capture());
+ Rect rect = new Rect(0, 0, 1, 1);
+
+ mSurfaceCallbackCaptor.getValue().onStableAreaChanged(rect, mMockOnDoneCallback);
+
+ verify(mSurfaceCallback).onStableAreaChanged(rect);
+ verify(mMockOnDoneCallback).onSuccess(any());
+ }
+
+ @Test
+ public void onStableAreaChanged_lifecycleNotCreated_doesNotDispatch_sendsAFailure()
+ throws RemoteException {
+ mAppManager.setSurfaceCallback(mSurfaceCallback);
+ verify(mMockAppHost).setSurfaceCallback(mSurfaceCallbackCaptor.capture());
+ Rect rect = new Rect(0, 0, 1, 1);
+
+ mSurfaceCallbackCaptor.getValue().onStableAreaChanged(rect, mMockOnDoneCallback);
+
+ verify(mSurfaceCallback, never()).onStableAreaChanged(any());
+ verify(mMockOnDoneCallback).onFailure(any());
+ }
+
+ @Test
+ public void onSurfaceDestroyed_dispatches()
+ throws RemoteException, BundlerException {
+ mTestCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.CREATED);
+ mAppManager.setSurfaceCallback(mSurfaceCallback);
+ verify(mMockAppHost).setSurfaceCallback(mSurfaceCallbackCaptor.capture());
+ SurfaceContainer surfaceContainer = new SurfaceContainer(null, 1, 2, 3);
+
+ mSurfaceCallbackCaptor.getValue().onSurfaceDestroyed(Bundleable.create(surfaceContainer),
+ mMockOnDoneCallback);
+
+ verify(mSurfaceCallback).onSurfaceDestroyed(any());
+ verify(mMockOnDoneCallback).onSuccess(any());
+ }
+
+ @Test
+ public void onSurfaceDestroyed_lifecycleNotCreated_doesNotDispatch_sendsAFailure()
+ throws RemoteException, BundlerException {
+ mAppManager.setSurfaceCallback(mSurfaceCallback);
+ verify(mMockAppHost).setSurfaceCallback(mSurfaceCallbackCaptor.capture());
+ SurfaceContainer surfaceContainer = new SurfaceContainer(null, 1, 2, 3);
+
+ mSurfaceCallbackCaptor.getValue().onSurfaceDestroyed(Bundleable.create(surfaceContainer),
+ mMockOnDoneCallback);
+
+ verify(mSurfaceCallback, never()).onSurfaceDestroyed(surfaceContainer);
+ verify(mMockOnDoneCallback).onFailure(any());
+ }
+
+ private static class NonBundleableTemplate implements Template {
+ NonBundleableTemplate(String s) {
+ }
+ }
+
+ private static class TestTemplate implements Template {
+ }
}
diff --git a/car/app/app/src/test/java/androidx/car/app/CarAppServiceTest.java b/car/app/app/src/test/java/androidx/car/app/CarAppServiceTest.java
index f5ea8a2..ae9ca49 100644
--- a/car/app/app/src/test/java/androidx/car/app/CarAppServiceTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/CarAppServiceTest.java
@@ -63,6 +63,8 @@
ICarHost mMockCarHost;
@Mock
DefaultLifecycleObserver mLifecycleObserver;
+ @Mock
+ IOnDoneCallback mMockOnDoneCallback;
private TestCarContext mCarContext;
private final Template mTemplate =
@@ -137,8 +139,8 @@
HandshakeInfo handshakeInfo = new HandshakeInfo(hostPackageName, hostApiLevel);
mCarAppService.setCurrentSession(null);
- carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mock(IOnDoneCallback.class));
- carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+ carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mMockOnDoneCallback);
+ carApp.onAppCreate(mMockCarHost, null, new Configuration(), mMockOnDoneCallback);
assertThat(
mCarAppService.getCurrentSession().getCarContext().getCarAppApiLevel()).isEqualTo(
@@ -148,7 +150,7 @@
@Test
public void onAppCreate_createsFirstScreen() throws RemoteException {
ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
- carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+ carApp.onAppCreate(mMockCarHost, null, new Configuration(), mMockOnDoneCallback);
assertThat(
mCarAppService
@@ -163,13 +165,12 @@
@Test
public void onAppCreate_withIntent_callsWithOnCreateScreenWithIntent() throws
RemoteException {
- IOnDoneCallback callback = mock(IOnDoneCallback.class);
ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
Intent intent = new Intent("Foo");
- carApp.onAppCreate(mMockCarHost, intent, new Configuration(), callback);
+ carApp.onAppCreate(mMockCarHost, intent, new Configuration(), mMockOnDoneCallback);
assertThat(mIntentSet).isEqualTo(intent);
- verify(callback).onSuccess(any());
+ verify(mMockOnDoneCallback).onSuccess(any());
}
@Test
@@ -209,9 +210,21 @@
carApp.onAppCreate(mMockCarHost, intent, new Configuration(), mock(IOnDoneCallback.class));
Intent intent2 = new Intent("Foo2");
- carApp.onNewIntent(intent2, mock(IOnDoneCallback.class));
+ carApp.onNewIntent(intent2, mMockOnDoneCallback);
assertThat(mIntentSet).isEqualTo(intent2);
+ verify(mMockOnDoneCallback).onSuccess(any());
+ }
+
+ @Test
+ public void onNewIntent_lifecycleNotCreated_doesNotDispatch_sendsError()
+ throws RemoteException {
+ ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+
+ carApp.onNewIntent(new Intent("Foo"), mMockOnDoneCallback);
+
+ assertThat(mIntentSet).isNull();
+ verify(mMockOnDoneCallback).onFailure(any());
}
@Test
@@ -224,6 +237,21 @@
}
@Test
+ public void onConfigurationChanged_lifecycleNotCreated_returnsAFailure()
+ throws RemoteException {
+ ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+
+ Configuration configuration = new Configuration();
+ configuration.setToDefaults();
+ configuration.setLocale(Locale.CANADA_FRENCH);
+
+ carApp.onConfigurationChanged(configuration, mMockOnDoneCallback);
+
+ assertThat(mCarContext).isNull();
+ verify(mMockOnDoneCallback).onFailure(any());
+ }
+
+ @Test
public void onConfigurationChanged_updatesTheConfiguration() throws RemoteException {
ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
@@ -232,10 +260,11 @@
configuration.setToDefaults();
configuration.setLocale(Locale.CANADA_FRENCH);
- carApp.onConfigurationChanged(configuration, mock(IOnDoneCallback.class));
+ carApp.onConfigurationChanged(configuration, mMockOnDoneCallback);
assertThat(mCarContext.getResources().getConfiguration().getLocales().get(0))
.isEqualTo(Locale.CANADA_FRENCH);
+ verify(mMockOnDoneCallback).onSuccess(any());
}
@Test
@@ -243,11 +272,10 @@
AppInfo appInfo = new AppInfo(3, 4, "foo");
mCarAppService.setAppInfo(appInfo);
ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
- IOnDoneCallback callback = mock(IOnDoneCallback.class);
- carApp.getAppInfo(callback);
+ carApp.getAppInfo(mMockOnDoneCallback);
- verify(callback).onSuccess(mBundleableArgumentCaptor.capture());
+ verify(mMockOnDoneCallback).onSuccess(mBundleableArgumentCaptor.capture());
AppInfo receivedAppInfo = (AppInfo) mBundleableArgumentCaptor.getValue().get();
assertThat(receivedAppInfo.getMinCarAppApiLevel())
.isEqualTo(appInfo.getMinCarAppApiLevel());
@@ -264,7 +292,7 @@
ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
HandshakeInfo handshakeInfo = new HandshakeInfo(hostPackageName, CarAppApiLevels.LEVEL_1);
- carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mock(IOnDoneCallback.class));
+ carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mMockOnDoneCallback);
assertThat(mCarAppService.getHostInfo().getPackageName()).isEqualTo(hostPackageName);
}
@@ -276,7 +304,7 @@
ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
HandshakeInfo handshakeInfo = new HandshakeInfo(hostPackageName, CarAppApiLevels.LEVEL_1);
- carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mock(IOnDoneCallback.class));
+ carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mMockOnDoneCallback);
assertThat(mCarAppService.getHandshakeInfo()).isNotNull();
assertThat(mCarAppService.getHandshakeInfo().getHostCarAppApiLevel()).isEqualTo(
handshakeInfo.getHostCarAppApiLevel());
@@ -293,10 +321,9 @@
HandshakeInfo handshakeInfo = new HandshakeInfo("bar",
appInfo.getMinCarAppApiLevel() - 1);
- IOnDoneCallback callback = mock(IOnDoneCallback.class);
- carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), callback);
+ carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mMockOnDoneCallback);
- verify(callback).onFailure(any());
+ verify(mMockOnDoneCallback).onFailure(any());
}
@Test
@@ -308,17 +335,16 @@
HandshakeInfo handshakeInfo = new HandshakeInfo("bar",
appInfo.getLatestCarAppApiLevel() + 1);
- IOnDoneCallback callback = mock(IOnDoneCallback.class);
- carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), callback);
+ carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mMockOnDoneCallback);
- verify(callback).onFailure(any());
+ verify(mMockOnDoneCallback).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));
+ carApp.onAppCreate(mMockCarHost, null, new Configuration(), mMockOnDoneCallback);
+ carApp.onAppStart(mMockOnDoneCallback);
mCarAppService.getCurrentSession().getLifecycle().addObserver(mLifecycleObserver);
@@ -331,8 +357,8 @@
public void onUnbind_rebind_callsOnCreateScreen() throws RemoteException, BundlerException {
ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
- carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
- carApp.onAppStart(mock(IOnDoneCallback.class));
+ carApp.onAppCreate(mMockCarHost, null, new Configuration(), mMockOnDoneCallback);
+ carApp.onAppStart(mMockOnDoneCallback);
Session currentSession = mCarAppService.getCurrentSession();
currentSession.getLifecycle().addObserver(mLifecycleObserver);
@@ -347,8 +373,8 @@
String hostPackageName = "com.google.projection.gearhead";
int hostApiLevel = CarAppApiLevels.LEVEL_1;
HandshakeInfo handshakeInfo = new HandshakeInfo(hostPackageName, hostApiLevel);
- carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mock(IOnDoneCallback.class));
- carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+ carApp.onHandshakeCompleted(Bundleable.create(handshakeInfo), mMockOnDoneCallback);
+ carApp.onAppCreate(mMockCarHost, null, new Configuration(), mMockOnDoneCallback);
currentSession = mCarAppService.getCurrentSession();
assertThat(currentSession.getCarContext().getCarService(
@@ -358,7 +384,7 @@
@Test
public void onUnbind_clearsScreenStack() throws RemoteException {
ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
- carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+ carApp.onAppCreate(mMockCarHost, null, new Configuration(), mMockOnDoneCallback);
Deque<Screen> screenStack =
mCarAppService.getCurrentSession().getCarContext().getCarService(
@@ -377,7 +403,7 @@
@Test
public void finish() throws RemoteException {
ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
- carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+ carApp.onAppCreate(mMockCarHost, null, new Configuration(), mMockOnDoneCallback);
mCarAppService.getCurrentSession().getCarContext().finishCarApp();
@@ -393,11 +419,189 @@
carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
assertThat(mIntentSet).isNull();
- IOnDoneCallback callback = mock(IOnDoneCallback.class);
Intent intent = new Intent("Foo");
- carApp.onNewIntent(intent, callback);
+ carApp.onNewIntent(intent, mMockOnDoneCallback);
assertThat(mIntentSet).isEqualTo(intent);
- verify(callback).onSuccess(any());
+ verify(mMockOnDoneCallback).onSuccess(any());
+ }
+
+ @Test
+ public void onNewIntent_notAtLeastCreated_doesCallSessionIntent_sendsFailure() throws
+ RemoteException {
+ ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+
+ Intent intent = new Intent("Foo");
+ carApp.onNewIntent(intent, mMockOnDoneCallback);
+
+ assertThat(mIntentSet).isNull();
+ verify(mMockOnDoneCallback).onFailure(any());
+ }
+
+ @Test
+ public void onNewIntent_destroyed_doesCallSessionIntent_sendsFailure() throws
+ RemoteException {
+ ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+
+ // onAppCreate must be called first to create the Session before onNewIntent.
+ Intent intent1 = new Intent();
+ carApp.onAppCreate(mMockCarHost, intent1, new Configuration(), mock(IOnDoneCallback.class));
+ assertThat(mIntentSet).isEqualTo(intent1);
+
+ mCarContext.getLifecycleOwner().mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
+
+ Intent intent2 = new Intent("Foo");
+ carApp.onNewIntent(intent2, mMockOnDoneCallback);
+
+ assertThat(mIntentSet).isEqualTo(intent1);
+ verify(mMockOnDoneCallback).onFailure(any());
+ }
+
+ @Test
+ public void onAppStart_movesLifecycle() throws RemoteException {
+ ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+ carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+
+ carApp.onAppStart(mMockOnDoneCallback);
+
+ verify(mMockOnDoneCallback).onSuccess(any());
+ assertThat(mCarContext.getLifecycleOwner().mRegistry.getCurrentState()).isEqualTo(
+ Lifecycle.State.STARTED);
+ }
+
+ @Test
+ public void onAppStart_notAtLeastCreated_doesNotMoveLifecycle_sendsFailure()
+ throws RemoteException {
+ ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+
+ carApp.onAppStart(mMockOnDoneCallback);
+
+ verify(mMockOnDoneCallback).onFailure(any());
+ assertThat(mCarContext).isNull();
+ }
+
+ @Test
+ public void onAppStart_destroyed_doesNotMoveLifecycle_sendsFailure()
+ throws RemoteException {
+ ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+ carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+ mCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.DESTROYED);
+
+ carApp.onAppStart(mMockOnDoneCallback);
+
+ verify(mMockOnDoneCallback).onFailure(any());
+ assertThat(mCarContext.getLifecycleOwner().mRegistry.getCurrentState()).isEqualTo(
+ Lifecycle.State.DESTROYED);
+ }
+
+ @Test
+ public void onAppResume_movesLifecycle() throws RemoteException {
+ ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+ carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+
+ carApp.onAppResume(mMockOnDoneCallback);
+
+ verify(mMockOnDoneCallback).onSuccess(any());
+ assertThat(mCarContext.getLifecycleOwner().mRegistry.getCurrentState()).isEqualTo(
+ Lifecycle.State.RESUMED);
+ }
+
+ @Test
+ public void onAppResume_notAtLeastCreated_doesNotMoveLifecycle_sendsFailure()
+ throws RemoteException {
+ ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+
+ carApp.onAppResume(mMockOnDoneCallback);
+
+ verify(mMockOnDoneCallback).onFailure(any());
+ assertThat(mCarContext).isNull();
+ }
+
+ @Test
+ public void onAppResume_destroyed_doesNotMoveLifecycle_sendsFailure()
+ throws RemoteException {
+ ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+ carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+ mCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.DESTROYED);
+
+ carApp.onAppResume(mMockOnDoneCallback);
+
+ verify(mMockOnDoneCallback).onFailure(any());
+ assertThat(mCarContext.getLifecycleOwner().mRegistry.getCurrentState()).isEqualTo(
+ Lifecycle.State.DESTROYED);
+ }
+
+ @Test
+ public void onAppPause_movesLifecycle() throws RemoteException {
+ ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+ carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+
+ carApp.onAppPause(mMockOnDoneCallback);
+
+ verify(mMockOnDoneCallback).onSuccess(any());
+ assertThat(mCarContext.getLifecycleOwner().mRegistry.getCurrentState()).isEqualTo(
+ Lifecycle.State.STARTED);
+ }
+
+ @Test
+ public void onAppPause_notAtLeastCreated_doesNotMoveLifecycle_sendsFailure()
+ throws RemoteException {
+ ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+
+ carApp.onAppPause(mMockOnDoneCallback);
+
+ verify(mMockOnDoneCallback).onFailure(any());
+ assertThat(mCarContext).isNull();
+ }
+
+ @Test
+ public void onAppPause_destroyed_doesNotMoveLifecycle_sendsFailure()
+ throws RemoteException {
+ ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+ carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+ mCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.DESTROYED);
+
+ carApp.onAppPause(mMockOnDoneCallback);
+
+ verify(mMockOnDoneCallback).onFailure(any());
+ assertThat(mCarContext.getLifecycleOwner().mRegistry.getCurrentState()).isEqualTo(
+ Lifecycle.State.DESTROYED);
+ }
+
+ @Test
+ public void onAppStop_movesLifecycle() throws RemoteException {
+ ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+ carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+
+ carApp.onAppStop(mMockOnDoneCallback);
+
+ verify(mMockOnDoneCallback).onSuccess(any());
+ assertThat(mCarContext.getLifecycleOwner().mRegistry.getCurrentState()).isEqualTo(
+ Lifecycle.State.CREATED);
+ }
+
+ @Test
+ public void onAppStop_notAtLeastCreated_doesNotMoveLifecycle_sendsFailure()
+ throws RemoteException {
+ ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+
+ carApp.onAppStop(mMockOnDoneCallback);
+
+ verify(mMockOnDoneCallback).onFailure(any());
+ assertThat(mCarContext).isNull();
+ }
+
+ @Test
+ public void onAppStop_destroyed_doesNotMoveLifecycle_sendsFailure()
+ throws RemoteException {
+ ICarApp carApp = (ICarApp) mCarAppService.onBind(null);
+ carApp.onAppCreate(mMockCarHost, null, new Configuration(), mock(IOnDoneCallback.class));
+ mCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.DESTROYED);
+
+ carApp.onAppStop(mMockOnDoneCallback);
+
+ verify(mMockOnDoneCallback).onFailure(any());
+ assertThat(mCarContext.getLifecycleOwner().mRegistry.getCurrentState()).isEqualTo(
+ Lifecycle.State.DESTROYED);
}
}
diff --git a/car/app/app/src/test/java/androidx/car/app/CarContextTest.java b/car/app/app/src/test/java/androidx/car/app/CarContextTest.java
index a0d08c2..a8eb9aa 100644
--- a/car/app/app/src/test/java/androidx/car/app/CarContextTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/CarContextTest.java
@@ -52,6 +52,7 @@
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.internal.DoNotInstrument;
+import java.lang.reflect.Field;
import java.util.Locale;
/** Tests for {@link CarContext}. */
@@ -402,4 +403,19 @@
verify(callback).handleOnBackPressed();
}
+
+ @Test
+ public void lifecycleDestroyed_removesHostBinders()
+ throws ReflectiveOperationException, RemoteException {
+ mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_CREATE);
+ Field field = CarContext.class.getDeclaredField("mHostDispatcher");
+ field.setAccessible(true);
+ HostDispatcher hostDispatcher = (HostDispatcher) field.get(mCarContext);
+
+ assertThat(hostDispatcher.getHost(CarContext.APP_SERVICE)).isNotNull();
+
+ mLifecycleOwner.mRegistry.handleLifecycleEvent(Event.ON_DESTROY);
+
+ assertThat(hostDispatcher.getHost(CarContext.APP_SERVICE)).isNull();
+ }
}
diff --git a/car/app/app/src/test/java/androidx/car/app/HostDispatcherTest.java b/car/app/app/src/test/java/androidx/car/app/HostDispatcherTest.java
index f0abe7e..c1a93fd 100644
--- a/car/app/app/src/test/java/androidx/car/app/HostDispatcherTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/HostDispatcherTest.java
@@ -100,13 +100,58 @@
}
@Test
- public void dispatch_callGoesToProperRemoteService() throws RemoteException {
- mHostDispatcher.dispatch(CarContext.APP_SERVICE,
- (IAppHost service) -> {
+ public void dispatchForResult_callGoesToProperRemoteService() throws RemoteException {
+ mHostDispatcher.dispatchForResult(CarContext.APP_SERVICE,
+ "test", (IAppHost service) -> {
service.invalidate();
return null;
- },
- "test");
+ }
+ );
+
+ verify(mMockAppHost).invalidate();
+ }
+
+ @Test
+ public void dispatchForResult_callThrowsSecurityException_throwsSecurityException() {
+ assertThrows(
+ SecurityException.class,
+ () -> mHostDispatcher.dispatchForResult(
+ CarContext.APP_SERVICE,
+ "test", (IAppHost service) -> {
+ throw new SecurityException();
+ }
+ ));
+ }
+
+ @Test
+ public void dispatchForResult_callThrowsRemoteException_throws() {
+ assertThrows(RemoteException.class, () -> mHostDispatcher.dispatchForResult(
+ CarContext.APP_SERVICE,
+ "test", (IAppHost service) -> {
+ throw new RemoteException();
+ }));
+ }
+
+ @Test
+ public void dispatchForResult_callThrowsRuntimeException_throwsHostException() {
+ assertThrows(
+ HostException.class,
+ () -> mHostDispatcher.dispatchForResult(
+ CarContext.APP_SERVICE,
+ "test", (IAppHost service) -> {
+ throw new IllegalStateException();
+ }
+ ));
+ }
+
+ @Test
+ public void dispatch_callGoesToProperRemoteService() throws RemoteException {
+ mHostDispatcher.dispatch(CarContext.APP_SERVICE,
+ "test", (IAppHost service) -> {
+ service.invalidate();
+ return null;
+ }
+ );
verify(mMockAppHost).invalidate();
}
@@ -117,22 +162,19 @@
SecurityException.class,
() -> mHostDispatcher.dispatch(
CarContext.APP_SERVICE,
- (IAppHost service) -> {
+ "test", (IAppHost service) -> {
throw new SecurityException();
- },
- "test"));
+ }
+ ));
}
@Test
- public void dispatch_callThrowsRemoteException_throwsHostException() {
- assertThrows(
- HostException.class,
- () -> mHostDispatcher.dispatch(
- CarContext.APP_SERVICE,
- (IAppHost service) -> {
- throw new RemoteException();
- },
- "test"));
+ public void dispatch_callThrowsRemoteException_doesNotCrash() {
+ mHostDispatcher.dispatch(
+ CarContext.APP_SERVICE,
+ "test", (IAppHost service) -> {
+ throw new RemoteException();
+ });
}
@Test
@@ -141,10 +183,10 @@
HostException.class,
() -> mHostDispatcher.dispatch(
CarContext.APP_SERVICE,
- (IAppHost service) -> {
+ "test", (IAppHost service) -> {
throw new IllegalStateException();
- },
- "test"));
+ }
+ ));
}
@Test
@@ -153,6 +195,7 @@
mHostDispatcher.resetHosts();
+ mHostDispatcher.setCarHost(mMockCarHost);
doThrow(new IllegalStateException()).when(mMockCarHost).getHost(any());
assertThrows(HostException.class, () -> mHostDispatcher.getHost(CarContext.APP_SERVICE));
@@ -168,15 +211,15 @@
}
@Test
- public void getHost_appHost_returnsProperHostService() {
+ public void getHost_appHost_returnsProperHostService() throws RemoteException {
assertThat(mHostDispatcher.getHost(CarContext.APP_SERVICE)).isEqualTo(mAppHost);
}
@Test
- public void getHost_appHost_hostThrowsRemoteException_throwsHostException()
+ public void getHost_appHost_hostThrowsRemoteException_throwsTheException()
throws RemoteException {
when(mMockCarHost.getHost(any())).thenThrow(new RemoteException());
- assertThrows(HostException.class, () -> mHostDispatcher.getHost(CarContext.APP_SERVICE));
+ assertThrows(RemoteException.class, () -> mHostDispatcher.getHost(CarContext.APP_SERVICE));
}
@Test
@@ -187,16 +230,16 @@
}
@Test
- public void getHost_navigationHost_returnsProperHostService() {
+ public void getHost_navigationHost_returnsProperHostService() throws RemoteException {
assertThat(mHostDispatcher.getHost(CarContext.NAVIGATION_SERVICE)).isEqualTo(
mNavigationHost);
}
@Test
- public void getHost_navigationHost_hostThrowsRemoteException_throwsHostException()
+ public void getHost_navigationHost_hostThrowsRemoteException()
throws RemoteException {
when(mMockCarHost.getHost(any())).thenThrow(new RemoteException());
- assertThrows(HostException.class,
+ assertThrows(RemoteException.class,
() -> mHostDispatcher.getHost(CarContext.NAVIGATION_SERVICE));
}
@@ -209,16 +252,16 @@
}
@Test
- public void getHost_afterReset_throwsHostException() {
+ public void getHost_afterReset_returnsNull() throws RemoteException {
mHostDispatcher.resetHosts();
- assertThrows(HostException.class, () -> mHostDispatcher.getHost(CarContext.APP_SERVICE));
+ assertThat(mHostDispatcher.getHost(CarContext.APP_SERVICE)).isNull();
}
@Test
- public void getHost_notBound_throwsHostException() {
+ public void getHost_notBound_returnsNull() throws RemoteException {
mHostDispatcher = new HostDispatcher();
- assertThrows(HostException.class, () -> mHostDispatcher.getHost(CarContext.APP_SERVICE));
+ assertThat(mHostDispatcher.getHost(CarContext.APP_SERVICE)).isNull();
}
}
diff --git a/car/app/app/src/test/java/androidx/car/app/navigation/NavigationManagerTest.java b/car/app/app/src/test/java/androidx/car/app/navigation/NavigationManagerTest.java
index f96c7ec..8e3b9c9 100644
--- a/car/app/app/src/test/java/androidx/car/app/navigation/NavigationManagerTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/navigation/NavigationManagerTest.java
@@ -38,6 +38,7 @@
import androidx.car.app.navigation.model.Trip;
import androidx.car.app.serialization.Bundleable;
import androidx.car.app.testing.TestCarContext;
+import androidx.lifecycle.Lifecycle;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Before;
@@ -86,12 +87,13 @@
.addStep(mStep, mStepTravelEstimate)
.setCurrentRoad(CURRENT_ROAD)
.build();
+ private TestCarContext mTestCarContext;
@Before
public void setUp() throws RemoteException {
MockitoAnnotations.initMocks(this);
- TestCarContext testCarContext =
+ mTestCarContext =
TestCarContext.createCarContext(ApplicationProvider.getApplicationContext());
INavigationHost navHostStub =
@@ -115,7 +117,8 @@
mHostDispatcher.setCarHost(mMockCarHost);
- mNavigationManager = NavigationManager.create(testCarContext, mHostDispatcher);
+ mNavigationManager = NavigationManager.create(mTestCarContext, mHostDispatcher,
+ mTestCarContext.getLifecycleOwner().mRegistry);
}
@Test
@@ -162,14 +165,43 @@
}
@Test
+ public void lifecycleDestroyed_callsOnStopNavigation() throws RemoteException {
+ mTestCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.CREATED);
+ mNavigationManager.setNavigationManagerCallback(new SynchronousExecutor(),
+ mNavigationListener);
+ mNavigationManager.navigationStarted();
+ verify(mMockNavHost).navigationStarted();
+
+ mTestCarContext.getLifecycleOwner().mRegistry.handleLifecycleEvent(
+ Lifecycle.Event.ON_DESTROY);
+
+ verify(mNavigationListener).onStopNavigation();
+ }
+
+ @Test
public void onStopNavigation_notNavigating() throws RemoteException {
+ mTestCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.CREATED);
mNavigationManager.setNavigationManagerCallback(mNavigationListener);
+
mNavigationManager.getIInterface().onStopNavigation(mock(IOnDoneCallback.class));
+
+ verify(mNavigationListener, never()).onStopNavigation();
+ }
+
+ @Test
+ public void onStopNavigation_lifecycleNotCreated_doesNotDispatch() throws RemoteException {
+ mNavigationManager.setNavigationManagerCallback(mNavigationListener);
+ mNavigationManager.navigationStarted();
+ verify(mMockNavHost).navigationStarted();
+
+ mNavigationManager.getIInterface().onStopNavigation(mock(IOnDoneCallback.class));
+
verify(mNavigationListener, never()).onStopNavigation();
}
@Test
public void onStopNavigation_navigating_restart() throws RemoteException {
+ mTestCarContext.getLifecycleOwner().mRegistry.setCurrentState(Lifecycle.State.CREATED);
InOrder inOrder = inOrder(mMockNavHost, mNavigationListener);
mNavigationManager.setNavigationManagerCallback(new SynchronousExecutor(),
@@ -186,6 +218,24 @@
}
@Test
+ public void onStopNavigation_noListener_doesNotThrow() throws RemoteException {
+ InOrder inOrder = inOrder(mMockNavHost, mNavigationListener);
+
+ mNavigationManager.setNavigationManagerCallback(new SynchronousExecutor(),
+ mNavigationListener);
+ mNavigationManager.navigationStarted();
+ inOrder.verify(mMockNavHost).navigationStarted();
+
+ mNavigationManager.onStopNavigation();
+ mNavigationManager.clearNavigationManagerCallback();
+ inOrder.verify(mNavigationListener).onStopNavigation();
+
+ mNavigationManager.getIInterface().onStopNavigation(mock(IOnDoneCallback.class));
+
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
public void onAutoDriveEnabled_callsListener() {
mNavigationManager.setNavigationManagerCallback(new SynchronousExecutor(),
mNavigationListener);
diff --git a/cleanBuild.sh b/cleanBuild.sh
index 55b7736..3ee4ae6 100755
--- a/cleanBuild.sh
+++ b/cleanBuild.sh
@@ -73,7 +73,7 @@
# ~/.gradle as the Gradle cache dir, which could surprise users because it might hold
# different state. So, we preemptively remove ~/.gradle too, just in case the user
# is going to want that for their following build
- rm ~/.gradle -rf
+ rm -rf ~/.gradle
# AGP should (also) do this automatically (b/170640263)
rm -rf appsearch/appsearch/.cxx
rm -rf appsearch/local-backend/.cxx
diff --git a/compose/androidview/androidview/integration-tests/androidview-demos/lint-baseline.xml b/compose/androidview/androidview/integration-tests/androidview-demos/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/androidview/androidview/integration-tests/androidview-demos/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/androidview/androidview/lint-baseline.xml b/compose/androidview/androidview/lint-baseline.xml
index 1538f8f..bf62433 100644
--- a/compose/androidview/androidview/lint-baseline.xml
+++ b/compose/androidview/androidview/lint-baseline.xml
@@ -1,26 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
+<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
<issue
id="BanUncheckedReflection"
diff --git a/compose/animation/animation-core/benchmark/build.gradle b/compose/animation/animation-core/benchmark/build.gradle
new file mode 100644
index 0000000..de70087
--- /dev/null
+++ b/compose/animation/animation-core/benchmark/build.gradle
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXUiPlugin")
+ id("org.jetbrains.kotlin.android")
+ id("androidx.benchmark")
+}
+
+dependencies {
+ kotlinPlugin project(":compose:compiler:compiler")
+
+ androidTestImplementation project(":benchmark:benchmark-junit4")
+ androidTestImplementation project(":compose:runtime:runtime")
+ androidTestImplementation project(":compose:benchmark-utils")
+ androidTestImplementation(ANDROIDX_TEST_RULES)
+ androidTestImplementation(KOTLIN_STDLIB)
+ androidTestImplementation(KOTLIN_TEST_COMMON)
+ androidTestImplementation(JUNIT)
+}
diff --git a/compose/animation/animation-core/benchmark/src/androidTest/AndroidManifest.xml b/compose/animation/animation-core/benchmark/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..fea4215
--- /dev/null
+++ b/compose/animation/animation-core/benchmark/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="androidx.compose.animation.core.benchmark">
+
+ <!--
+ ~ Important: disable debuggable for accurate performance results
+ -->
+ <application
+ android:debuggable="false"
+ tools:replace="android:debuggable">
+ <!-- enable profileableByShell for non-intrusive profiling tools -->
+ <!--suppress AndroidElementNotAllowed -->
+ <profileable android:shell="true"/>
+ </application>
+</manifest>
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/AnimationBenchmark.kt b/compose/animation/animation-core/benchmark/src/androidTest/java/androidx/compose/animation/core/benchmark/AnimationBenchmark.kt
similarity index 98%
rename from compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/AnimationBenchmark.kt
rename to compose/animation/animation-core/benchmark/src/androidTest/java/androidx/compose/animation/core/benchmark/AnimationBenchmark.kt
index 3911015..f004d5e 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/AnimationBenchmark.kt
+++ b/compose/animation/animation-core/benchmark/src/androidTest/java/androidx/compose/animation/core/benchmark/AnimationBenchmark.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.ui.benchmark.test
+package androidx.compose.animation.core.benchmark
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
@@ -31,11 +31,11 @@
import androidx.compose.animation.core.VectorizedSpringSpec
import androidx.compose.animation.core.VectorizedTweenSpec
import androidx.compose.animation.core.createAnimation
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import androidx.test.ext.junit.runners.AndroidJUnit4
@LargeTest
@RunWith(AndroidJUnit4::class)
@@ -162,4 +162,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/SpringEstimationBenchmark.kt b/compose/animation/animation-core/benchmark/src/androidTest/java/androidx/compose/animation/core/benchmark/SpringEstimationBenchmark.kt
similarity index 97%
rename from compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/SpringEstimationBenchmark.kt
rename to compose/animation/animation-core/benchmark/src/androidTest/java/androidx/compose/animation/core/benchmark/SpringEstimationBenchmark.kt
index 743b2bc..7c72011 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/SpringEstimationBenchmark.kt
+++ b/compose/animation/animation-core/benchmark/src/androidTest/java/androidx/compose/animation/core/benchmark/SpringEstimationBenchmark.kt
@@ -14,16 +14,16 @@
* limitations under the License.
*/
-package androidx.ui.benchmark.test
+package androidx.compose.animation.core.benchmark
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.compose.animation.core.estimateAnimationDurationMillis
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlin.math.sqrt
@LargeTest
diff --git a/compose/animation/animation-core/benchmark/src/main/AndroidManifest.xml b/compose/animation/animation-core/benchmark/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..e2b118f
--- /dev/null
+++ b/compose/animation/animation-core/benchmark/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.compose.animation.core.benchmark">
+ <application/>
+</manifest>
diff --git a/compose/animation/animation-core/lint-baseline.xml b/compose/animation/animation-core/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/animation/animation-core/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/animation/animation-core/samples/lint-baseline.xml b/compose/animation/animation-core/samples/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/animation/animation-core/samples/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/animation/animation/integration-tests/animation-demos/lint-baseline.xml b/compose/animation/animation/integration-tests/animation-demos/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/animation/animation/integration-tests/animation-demos/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/animation/animation/lint-baseline.xml b/compose/animation/animation/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/animation/animation/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/animation/animation/samples/lint-baseline.xml b/compose/animation/animation/samples/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/animation/animation/samples/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/benchmark-utils/OWNERS b/compose/benchmark-utils/OWNERS
new file mode 100644
index 0000000..305021a
--- /dev/null
+++ b/compose/benchmark-utils/OWNERS
@@ -0,0 +1,2 @@
+pavlis@google.com
+jellefresen@google.com
diff --git a/compose/benchmark-utils/benchmark/build.gradle b/compose/benchmark-utils/benchmark/build.gradle
new file mode 100644
index 0000000..699caad
--- /dev/null
+++ b/compose/benchmark-utils/benchmark/build.gradle
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXUiPlugin")
+ id("org.jetbrains.kotlin.android")
+ id("androidx.benchmark")
+}
+
+dependencies {
+ kotlinPlugin project(":compose:compiler:compiler")
+
+ androidTestImplementation project(":benchmark:benchmark-junit4")
+ androidTestImplementation project(":compose:benchmark-utils")
+ androidTestImplementation project(":compose:runtime:runtime")
+ androidTestImplementation project(":compose:foundation:foundation-layout")
+ androidTestImplementation(ANDROIDX_TEST_RULES)
+ androidTestImplementation(KOTLIN_STDLIB)
+ androidTestImplementation(KOTLIN_TEST_COMMON)
+ androidTestImplementation(JUNIT)
+}
diff --git a/compose/benchmark-utils/benchmark/src/androidTest/AndroidManifest.xml b/compose/benchmark-utils/benchmark/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..b4b3fae
--- /dev/null
+++ b/compose/benchmark-utils/benchmark/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="androidx.compose.benchmarkutils.benchmark">
+
+ <!--
+ ~ Important: disable debuggable for accurate performance results
+ -->
+ <application
+ android:debuggable="false"
+ tools:replace="android:debuggable">
+ <!-- enable profileableByShell for non-intrusive profiling tools -->
+ <!--suppress AndroidElementNotAllowed -->
+ <profileable android:shell="true"/>
+ </application>
+</manifest>
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/EmptyBenchmark.kt b/compose/benchmark-utils/benchmark/src/androidTest/java/androidx/compose/benchmarkutils/benchmark/EmptyBenchmark.kt
similarity index 88%
rename from compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/EmptyBenchmark.kt
rename to compose/benchmark-utils/benchmark/src/androidTest/java/androidx/compose/benchmarkutils/benchmark/EmptyBenchmark.kt
index f0d978c..a3ffd44 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/EmptyBenchmark.kt
+++ b/compose/benchmark-utils/benchmark/src/androidTest/java/androidx/compose/benchmarkutils/benchmark/EmptyBenchmark.kt
@@ -14,8 +14,10 @@
* limitations under the License.
*/
-package androidx.compose.ui
+package androidx.compose.ui.testutils.benchmark
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.ComposeTestCase
import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
import androidx.compose.testutils.benchmark.benchmarkDrawPerf
import androidx.compose.testutils.benchmark.benchmarkFirstCompose
@@ -23,7 +25,6 @@
import androidx.compose.testutils.benchmark.benchmarkFirstLayout
import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
import androidx.test.filters.LargeTest
-import androidx.ui.integration.test.core.EmptyTestCase
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -62,4 +63,10 @@
fun draw() {
benchmarkRule.benchmarkDrawPerf(textCaseFactory)
}
+}
+
+class EmptyTestCase : ComposeTestCase {
+ @Composable
+ override fun Content() {
+ }
}
\ No newline at end of file
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/EmptyFirstFastBenchmark.kt b/compose/benchmark-utils/benchmark/src/androidTest/java/androidx/compose/benchmarkutils/benchmark/EmptyFirstFastBenchmark.kt
similarity index 97%
rename from compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/EmptyFirstFastBenchmark.kt
rename to compose/benchmark-utils/benchmark/src/androidTest/java/androidx/compose/benchmarkutils/benchmark/EmptyFirstFastBenchmark.kt
index 9bc03cd..e5b3694 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/EmptyFirstFastBenchmark.kt
+++ b/compose/benchmark-utils/benchmark/src/androidTest/java/androidx/compose/benchmarkutils/benchmark/EmptyFirstFastBenchmark.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.compose.ui
+package androidx.compose.ui.testutils.benchmark
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
diff --git a/compose/benchmark-utils/benchmark/src/main/AndroidManifest.xml b/compose/benchmark-utils/benchmark/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..2157585
--- /dev/null
+++ b/compose/benchmark-utils/benchmark/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.compose.benchmarkutils.benchmark">
+ <application/>
+</manifest>
diff --git a/compose/benchmark-utils/build.gradle b/compose/benchmark-utils/build.gradle
new file mode 100644
index 0000000..a6654e3
--- /dev/null
+++ b/compose/benchmark-utils/build.gradle
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+import androidx.build.AndroidXUiPlugin
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXUiPlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ kotlinPlugin project(":compose:compiler:compiler")
+
+ api("androidx.activity:activity:1.2.0")
+ api(project(":compose:test-utils"))
+ api(project(":benchmark:benchmark-junit4"))
+
+ implementation(KOTLIN_STDLIB_COMMON)
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:ui:ui"))
+ implementation(ANDROIDX_TEST_RULES)
+
+ // This has stub APIs for access to legacy Android APIs, so we don't want
+ // any dependency on this module.
+ compileOnly(project(":compose:ui:ui-android-stubs"))
+}
\ No newline at end of file
diff --git a/compose/benchmark-utils/src/androidTest/AndroidManifest.xml b/compose/benchmark-utils/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..6aca4a1
--- /dev/null
+++ b/compose/benchmark-utils/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.compose.benchmarkutils"/>
diff --git a/compose/benchmark-utils/src/main/AndroidManifest.xml b/compose/benchmark-utils/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..19f27c9
--- /dev/null
+++ b/compose/benchmark-utils/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.compose.benchmarkutils"/>
diff --git a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/AndroidBenchmarkRule.android.kt b/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/AndroidBenchmarkRule.kt
similarity index 100%
rename from compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/AndroidBenchmarkRule.android.kt
rename to compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/AndroidBenchmarkRule.kt
diff --git a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/BenchmarkFirstExtensions.android.kt b/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarkFirstExtensions.kt
similarity index 100%
rename from compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/BenchmarkFirstExtensions.android.kt
rename to compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarkFirstExtensions.kt
diff --git a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/BenchmarkHelpers.android.kt b/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarkHelpers.kt
similarity index 91%
rename from compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/BenchmarkHelpers.android.kt
rename to compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarkHelpers.kt
index b0e52249..ec3c2e7 100644
--- a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/BenchmarkHelpers.android.kt
+++ b/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarkHelpers.kt
@@ -16,10 +16,10 @@
package androidx.compose.testutils.benchmark
-import android.annotation.TargetApi
import android.graphics.Picture
import android.graphics.RenderNode
import android.os.Build
+import androidx.annotation.RequiresApi
import androidx.compose.ui.graphics.Canvas
// We must separate the use of RenderNode so that it isn't referenced in any
@@ -31,15 +31,14 @@
}
fun DrawCapture(): DrawCapture {
- val supportsRenderNode = Build.VERSION.SDK_INT >= 29
- return if (supportsRenderNode) {
+ return if (Build.VERSION.SDK_INT >= 29) {
RenderNodeCapture()
} else {
PictureCapture()
}
}
-@TargetApi(Build.VERSION_CODES.Q)
+@RequiresApi(Build.VERSION_CODES.Q)
private class RenderNodeCapture : DrawCapture {
private val renderNode = RenderNode("Test")
diff --git a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/BenchmarksExtensions.android.kt b/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarksExtensions.kt
similarity index 100%
rename from compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/BenchmarksExtensions.android.kt
rename to compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/BenchmarksExtensions.kt
diff --git a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/ComposeBenchmarkRule.android.kt b/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/ComposeBenchmarkRule.kt
similarity index 100%
rename from compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/ComposeBenchmarkRule.android.kt
rename to compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/ComposeBenchmarkRule.kt
diff --git a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/android/AndroidTestCase.android.kt b/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/android/AndroidTestCase.kt
similarity index 100%
rename from compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/android/AndroidTestCase.android.kt
rename to compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/android/AndroidTestCase.kt
diff --git a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/android/AndroidTestCaseRunner.android.kt b/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/android/AndroidTestCaseRunner.kt
similarity index 94%
rename from compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/android/AndroidTestCaseRunner.android.kt
rename to compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/android/AndroidTestCaseRunner.kt
index da23f9e..0376b84 100644
--- a/compose/test-utils/src/androidMain/kotlin/androidx/compose/testutils/benchmark/android/AndroidTestCaseRunner.android.kt
+++ b/compose/benchmark-utils/src/main/java/androidx/compose/testutils/benchmark/android/AndroidTestCaseRunner.kt
@@ -17,7 +17,6 @@
package androidx.compose.testutils.benchmark.android
import android.R
-import android.annotation.TargetApi
import android.app.Activity
import android.graphics.Canvas
import android.graphics.Picture
@@ -26,6 +25,7 @@
import android.util.DisplayMetrics
import android.view.View
import android.view.ViewGroup
+import androidx.annotation.RequiresApi
class AndroidTestCaseRunner<T : AndroidTestCase>(
private val testCaseFactory: () -> T,
@@ -39,11 +39,9 @@
private var view: ViewGroup? = null
- private val supportsRenderNode = Build.VERSION.SDK_INT >= 29
-
private val screenWithSpec: Int
private val screenHeightSpec: Int
- private val capture = if (supportsRenderNode) RenderNodeCapture() else PictureCapture()
+ private val capture = if (Build.VERSION.SDK_INT >= 29) RenderNodeCapture() else PictureCapture()
private var canvas: Canvas? = null
private var testCase: T? = null
@@ -156,7 +154,7 @@
fun endRecording()
}
-@TargetApi(Build.VERSION_CODES.Q)
+@RequiresApi(Build.VERSION_CODES.Q)
private class RenderNodeCapture : DrawCapture {
private val renderNode = RenderNode("Test")
diff --git a/compose/compiler/compiler-hosted/integration-tests/kotlin-compiler-repackaged/lint-baseline.xml b/compose/compiler/compiler-hosted/integration-tests/kotlin-compiler-repackaged/lint-baseline.xml
deleted file mode 100644
index 297ae16..0000000
--- a/compose/compiler/compiler-hosted/integration-tests/kotlin-compiler-repackaged/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" version="4.2.0-beta02">
-
-</issues>
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 8f1aa4b..0000000
--- a/compose/compiler/compiler-hosted/integration-tests/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/compose/compiler/compiler-hosted/lint-baseline.xml b/compose/compiler/compiler-hosted/lint-baseline.xml
deleted file mode 100644
index 297ae16..0000000
--- a/compose/compiler/compiler-hosted/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" version="4.2.0-beta02">
-
-</issues>
diff --git a/compose/compiler/compiler/lint-baseline.xml b/compose/compiler/compiler/lint-baseline.xml
deleted file mode 100644
index 297ae16..0000000
--- a/compose/compiler/compiler/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" version="4.2.0-beta02">
-
-</issues>
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt
index 385b464..9eae1d7 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt
@@ -70,7 +70,10 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusOrder
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
@@ -107,6 +110,7 @@
val italicFont = FontFamily(Font("NotoSans-Italic.ttf"))
+@OptIn(ExperimentalComposeUiApi::class)
fun main() {
Window(title, IntSize(1024, 850)) {
App()
@@ -169,13 +173,11 @@
)
}
+@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun ScrollableContent(scrollState: ScrollState) {
val amount = remember { mutableStateOf(0) }
val animation = remember { mutableStateOf(true) }
- val text = remember {
- mutableStateOf("Hello \uD83E\uDDD1\uD83C\uDFFF\u200D\uD83E\uDDB0\nПривет")
- }
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {
Text(
text = "Привет! ä½ å¥½! Desktop Compose ${amount.value}",
@@ -407,6 +409,10 @@
label = { Text(text = "Input1") }
)
+ val (focusItem1, focusItem2) = FocusRequester.createRefs()
+ val text = remember {
+ mutableStateOf("Hello \uD83E\uDDD1\uD83C\uDFFF\u200D\uD83E\uDDB0")
+ }
TextField(
value = text.value,
onValueChange = { text.value = it },
@@ -414,6 +420,7 @@
placeholder = {
Text(text = "Important input")
},
+ maxLines = 1,
modifier = Modifier.shortcuts {
on(Key.MetaLeft + Key.ShiftLeft + Key.Enter) {
text.value = "Cleared with shift!"
@@ -421,9 +428,27 @@
on(Key.MetaLeft + Key.Enter) {
text.value = "Cleared!"
}
+ }.focusOrder(focusItem1) {
+ next = focusItem2
}
)
+ var text2 by remember {
+ val initText = buildString {
+ (1..1000).forEach {
+ append("$it\n")
+ }
+ }
+ mutableStateOf(initText)
+ }
+ TextField(
+ text2,
+ modifier = Modifier.height(200.dp).focusOrder(focusItem2) {
+ previous = focusItem1
+ },
+ onValueChange = { text2 = it }
+ )
+
Row {
Image(
imageResource("androidx/compose/desktop/example/circus.jpg"),
diff --git a/compose/foundation/foundation-layout/benchmark/build.gradle b/compose/foundation/foundation-layout/benchmark/build.gradle
index e54e54f..cb7cc0e 100644
--- a/compose/foundation/foundation-layout/benchmark/build.gradle
+++ b/compose/foundation/foundation-layout/benchmark/build.gradle
@@ -33,7 +33,7 @@
androidTestImplementation project(":compose:foundation:foundation-layout")
androidTestImplementation project(":compose:material:material")
androidTestImplementation project(":compose:runtime:runtime")
- androidTestImplementation project(":compose:test-utils")
+ androidTestImplementation project(":compose:benchmark-utils")
androidTestImplementation(ANDROIDX_TEST_RULES)
androidTestImplementation(JUNIT)
androidTestImplementation(KOTLIN_STDLIB)
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/SpacingBenchmark.kt b/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/SpacingBenchmark.kt
similarity index 98%
rename from compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/SpacingBenchmark.kt
rename to compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/SpacingBenchmark.kt
index 5eae5b2..ca00d7b 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/SpacingBenchmark.kt
+++ b/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/SpacingBenchmark.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.ui.benchmark.test
+package androidx.compose.foundation.layout.benchmark
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/WithConstraintsBenchmark.kt b/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/WithConstraintsBenchmark.kt
similarity index 96%
rename from compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/WithConstraintsBenchmark.kt
rename to compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/WithConstraintsBenchmark.kt
index b57c79a..79a5adb 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/WithConstraintsBenchmark.kt
+++ b/compose/foundation/foundation-layout/benchmark/src/androidTest/java/androidx/compose/foundation/layout/benchmark/WithConstraintsBenchmark.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.compose.ui
+package androidx.compose.foundation.layout.benchmark
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
@@ -31,6 +31,8 @@
import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
import androidx.compose.testutils.benchmark.toggleStateBenchmarkComposeMeasureLayout
import androidx.compose.testutils.benchmark.toggleStateBenchmarkMeasureLayout
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
diff --git a/compose/foundation/foundation-layout/integration-tests/layout-demos/lint-baseline.xml b/compose/foundation/foundation-layout/integration-tests/layout-demos/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/foundation/foundation-layout/integration-tests/layout-demos/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/foundation/foundation-layout/lint-baseline.xml b/compose/foundation/foundation-layout/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/foundation/foundation-layout/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/foundation/foundation-layout/samples/lint-baseline.xml b/compose/foundation/foundation-layout/samples/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/foundation/foundation-layout/samples/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
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 9ca1b6ff..989ed4f 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
@@ -3588,6 +3588,24 @@
}
@Test
+ fun testRow_withArrangementSpacing() = with(density) {
+ val spacing = 5
+ val childSize = 10
+ testIntrinsics(
+ @Composable {
+ Row(horizontalArrangement = Arrangement.spacedBy(spacing.toDp())) {
+ Box(Modifier.size(childSize.toDp()))
+ Box(Modifier.size(childSize.toDp()))
+ Box(Modifier.size(childSize.toDp()))
+ }
+ }
+ ) { minIntrinsicWidth, _, maxIntrinsicWidth, _ ->
+ assertEquals(childSize * 3 + 2 * spacing, minIntrinsicWidth(Constraints.Infinity))
+ assertEquals(childSize * 3 + 2 * spacing, maxIntrinsicWidth(Constraints.Infinity))
+ }
+ }
+
+ @Test
fun testColumn_withNoWeightChildren_hasCorrectIntrinsicMeasurements() = with(density) {
testIntrinsics(
@Composable {
@@ -3917,6 +3935,24 @@
}
@Test
+ fun testColumn_withArrangementSpacing() = with(density) {
+ val spacing = 5
+ val childSize = 10
+ testIntrinsics(
+ @Composable {
+ Column(verticalArrangement = Arrangement.spacedBy(spacing.toDp())) {
+ Box(Modifier.size(childSize.toDp()))
+ Box(Modifier.size(childSize.toDp()))
+ Box(Modifier.size(childSize.toDp()))
+ }
+ }
+ ) { _, minIntrinsicHeight, _, maxIntrinsicHeight ->
+ assertEquals(childSize * 3 + 2 * spacing, minIntrinsicHeight(Constraints.Infinity))
+ assertEquals(childSize * 3 + 2 * spacing, maxIntrinsicHeight(Constraints.Infinity))
+ }
+ }
+
+ @Test
fun testRow_withWIHOChild_hasCorrectIntrinsicMeasurements() = with(density) {
val dividerWidth = 10.dp
val rowWidth = 40.dp
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
index c2603f5..bfa647b 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
@@ -268,22 +268,38 @@
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
- ) = MinIntrinsicWidthMeasureBlock(orientation)(measurables, height)
+ ) = MinIntrinsicWidthMeasureBlock(orientation)(
+ measurables,
+ height,
+ arrangementSpacing.roundToPx()
+ )
override fun IntrinsicMeasureScope.minIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
- ) = MinIntrinsicHeightMeasureBlock(orientation)(measurables, width)
+ ) = MinIntrinsicHeightMeasureBlock(orientation)(
+ measurables,
+ width,
+ arrangementSpacing.roundToPx()
+ )
override fun IntrinsicMeasureScope.maxIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
- ) = MaxIntrinsicWidthMeasureBlock(orientation)(measurables, height)
+ ) = MaxIntrinsicWidthMeasureBlock(orientation)(
+ measurables,
+ height,
+ arrangementSpacing.roundToPx()
+ )
override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
- ) = MaxIntrinsicHeightMeasureBlock(orientation)(measurables, width)
+ ) = MaxIntrinsicHeightMeasureBlock(orientation)(
+ measurables,
+ width,
+ arrangementSpacing.roundToPx()
+ )
}
}
@@ -559,90 +575,98 @@
}
private object IntrinsicMeasureBlocks {
- val HorizontalMinWidth: (List<IntrinsicMeasurable>, Int) -> Int =
- { measurables, availableHeight ->
+ val HorizontalMinWidth: (List<IntrinsicMeasurable>, Int, Int) -> Int =
+ { measurables, availableHeight, mainAxisSpacing ->
intrinsicSize(
measurables,
{ h -> minIntrinsicWidth(h) },
{ w -> maxIntrinsicHeight(w) },
availableHeight,
+ mainAxisSpacing,
LayoutOrientation.Horizontal,
LayoutOrientation.Horizontal
)
}
- val VerticalMinWidth: (List<IntrinsicMeasurable>, Int) -> Int =
- { measurables, availableHeight ->
+ val VerticalMinWidth: (List<IntrinsicMeasurable>, Int, Int) -> Int =
+ { measurables, availableHeight, mainAxisSpacing ->
intrinsicSize(
measurables,
{ h -> minIntrinsicWidth(h) },
{ w -> maxIntrinsicHeight(w) },
availableHeight,
+ mainAxisSpacing,
LayoutOrientation.Vertical,
LayoutOrientation.Horizontal
)
}
- val HorizontalMinHeight: (List<IntrinsicMeasurable>, Int) -> Int =
- { measurables, availableWidth ->
+ val HorizontalMinHeight: (List<IntrinsicMeasurable>, Int, Int) -> Int =
+ { measurables, availableWidth, mainAxisSpacing ->
intrinsicSize(
measurables,
{ w -> minIntrinsicHeight(w) },
{ h -> maxIntrinsicWidth(h) },
availableWidth,
+ mainAxisSpacing,
LayoutOrientation.Horizontal,
LayoutOrientation.Vertical
)
}
- val VerticalMinHeight: (List<IntrinsicMeasurable>, Int) -> Int =
- { measurables, availableWidth ->
+ val VerticalMinHeight: (List<IntrinsicMeasurable>, Int, Int) -> Int =
+ { measurables, availableWidth, mainAxisSpacing ->
intrinsicSize(
measurables,
{ w -> minIntrinsicHeight(w) },
{ h -> maxIntrinsicWidth(h) },
availableWidth,
+ mainAxisSpacing,
LayoutOrientation.Vertical,
LayoutOrientation.Vertical
)
}
- val HorizontalMaxWidth: (List<IntrinsicMeasurable>, Int) -> Int =
- { measurables, availableHeight ->
+ val HorizontalMaxWidth: (List<IntrinsicMeasurable>, Int, Int) -> Int =
+ { measurables, availableHeight, mainAxisSpacing ->
intrinsicSize(
measurables,
{ h -> maxIntrinsicWidth(h) },
{ w -> maxIntrinsicHeight(w) },
availableHeight,
+ mainAxisSpacing,
LayoutOrientation.Horizontal,
LayoutOrientation.Horizontal
)
}
- val VerticalMaxWidth: (List<IntrinsicMeasurable>, Int) -> Int =
- { measurables, availableHeight ->
+ val VerticalMaxWidth: (List<IntrinsicMeasurable>, Int, Int) -> Int =
+ { measurables, availableHeight, mainAxisSpacing ->
intrinsicSize(
measurables,
{ h -> maxIntrinsicWidth(h) },
{ w -> maxIntrinsicHeight(w) },
availableHeight,
+ mainAxisSpacing,
LayoutOrientation.Vertical,
LayoutOrientation.Horizontal
)
}
- val HorizontalMaxHeight: (List<IntrinsicMeasurable>, Int) -> Int =
- { measurables, availableWidth ->
+ val HorizontalMaxHeight: (List<IntrinsicMeasurable>, Int, Int) -> Int =
+ { measurables, availableWidth, mainAxisSpacing ->
intrinsicSize(
measurables,
{ w -> maxIntrinsicHeight(w) },
{ h -> maxIntrinsicWidth(h) },
availableWidth,
+ mainAxisSpacing,
LayoutOrientation.Horizontal,
LayoutOrientation.Vertical
)
}
- val VerticalMaxHeight: (List<IntrinsicMeasurable>, Int) -> Int =
- { measurables, availableWidth ->
+ val VerticalMaxHeight: (List<IntrinsicMeasurable>, Int, Int) -> Int =
+ { measurables, availableWidth, mainAxisSpacing ->
intrinsicSize(
measurables,
{ w -> maxIntrinsicHeight(w) },
{ h -> maxIntrinsicWidth(h) },
availableWidth,
+ mainAxisSpacing,
LayoutOrientation.Vertical,
LayoutOrientation.Vertical
)
@@ -654,10 +678,11 @@
intrinsicMainSize: IntrinsicMeasurable.(Int) -> Int,
intrinsicCrossSize: IntrinsicMeasurable.(Int) -> Int,
crossAxisAvailable: Int,
+ mainAxisSpacing: Int,
layoutOrientation: LayoutOrientation,
intrinsicOrientation: LayoutOrientation
) = if (layoutOrientation == intrinsicOrientation) {
- intrinsicMainAxisSize(children, intrinsicMainSize, crossAxisAvailable)
+ intrinsicMainAxisSize(children, intrinsicMainSize, crossAxisAvailable, mainAxisSpacing)
} else {
intrinsicCrossAxisSize(children, intrinsicCrossSize, intrinsicMainSize, crossAxisAvailable)
}
@@ -665,7 +690,8 @@
private fun intrinsicMainAxisSize(
children: List<IntrinsicMeasurable>,
mainAxisSize: IntrinsicMeasurable.(Int) -> Int,
- crossAxisAvailable: Int
+ crossAxisAvailable: Int,
+ mainAxisSpacing: Int
): Int {
var weightUnitSpace = 0
var fixedSpace = 0
@@ -680,7 +706,8 @@
weightUnitSpace = max(weightUnitSpace, (size / weight).roundToInt())
}
}
- return (weightUnitSpace * totalWeight).roundToInt() + fixedSpace
+ return (weightUnitSpace * totalWeight).roundToInt() + fixedSpace +
+ (children.size - 1) * mainAxisSpacing
}
private fun intrinsicCrossAxisSize(
diff --git a/compose/foundation/foundation/api/1.0.0-beta02.txt b/compose/foundation/foundation/api/1.0.0-beta02.txt
index 132be90..76b8d5b 100644
--- a/compose/foundation/foundation/api/1.0.0-beta02.txt
+++ b/compose/foundation/foundation/api/1.0.0-beta02.txt
@@ -558,9 +558,6 @@
package androidx.compose.foundation.text {
- public final class AndroidCoreTextField_androidKt {
- }
-
public final class BasicTextFieldKt {
method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
@@ -593,6 +590,12 @@
method public static void appendInlineContent(androidx.compose.ui.text.AnnotatedString.Builder, String id, optional String alternateText);
}
+ public final class KeyMappingKt {
+ }
+
+ public final class KeyMapping_androidKt {
+ }
+
public interface KeyboardActionScope {
method public void defaultKeyboardAction(androidx.compose.ui.text.input.ImeAction imeAction);
}
@@ -645,6 +648,9 @@
public final class MaxLinesHeightModifierKt {
}
+ public final class StringHelpers_jvmKt {
+ }
+
public final class TextFieldCursorKt {
}
@@ -654,6 +660,12 @@
public final class TextFieldGestureModifiersKt {
}
+ public final class TextFieldKeyInputKt {
+ }
+
+ public final class TextFieldKeyInput_androidKt {
+ }
+
public final class TextFieldPressGestureFilterKt {
}
@@ -705,6 +717,9 @@
public final class TextFieldSelectionManagerKt {
}
+ public final class TextFieldSelectionManager_androidKt {
+ }
+
@androidx.compose.runtime.Immutable public final class TextSelectionColors {
method public long getBackgroundColor-0d7_KjU();
method public long getHandleColor-0d7_KjU();
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 132be90..76b8d5b 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -558,9 +558,6 @@
package androidx.compose.foundation.text {
- public final class AndroidCoreTextField_androidKt {
- }
-
public final class BasicTextFieldKt {
method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
@@ -593,6 +590,12 @@
method public static void appendInlineContent(androidx.compose.ui.text.AnnotatedString.Builder, String id, optional String alternateText);
}
+ public final class KeyMappingKt {
+ }
+
+ public final class KeyMapping_androidKt {
+ }
+
public interface KeyboardActionScope {
method public void defaultKeyboardAction(androidx.compose.ui.text.input.ImeAction imeAction);
}
@@ -645,6 +648,9 @@
public final class MaxLinesHeightModifierKt {
}
+ public final class StringHelpers_jvmKt {
+ }
+
public final class TextFieldCursorKt {
}
@@ -654,6 +660,12 @@
public final class TextFieldGestureModifiersKt {
}
+ public final class TextFieldKeyInputKt {
+ }
+
+ public final class TextFieldKeyInput_androidKt {
+ }
+
public final class TextFieldPressGestureFilterKt {
}
@@ -705,6 +717,9 @@
public final class TextFieldSelectionManagerKt {
}
+ public final class TextFieldSelectionManager_androidKt {
+ }
+
@androidx.compose.runtime.Immutable public final class TextSelectionColors {
method public long getBackgroundColor-0d7_KjU();
method public long getHandleColor-0d7_KjU();
diff --git a/compose/foundation/foundation/api/public_plus_experimental_1.0.0-beta02.txt b/compose/foundation/foundation/api/public_plus_experimental_1.0.0-beta02.txt
index 0fac70c..058fea0 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_1.0.0-beta02.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_1.0.0-beta02.txt
@@ -591,9 +591,6 @@
package androidx.compose.foundation.text {
- public final class AndroidCoreTextField_androidKt {
- }
-
public final class BasicTextFieldKt {
method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
@@ -629,6 +626,12 @@
@kotlin.RequiresOptIn(message="Internal/Unstable API for use only between foundation modules sharing " + "the same exact version, subject to change without notice.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface InternalFoundationTextApi {
}
+ public final class KeyMappingKt {
+ }
+
+ public final class KeyMapping_androidKt {
+ }
+
public interface KeyboardActionScope {
method public void defaultKeyboardAction(androidx.compose.ui.text.input.ImeAction imeAction);
}
@@ -681,6 +684,9 @@
public final class MaxLinesHeightModifierKt {
}
+ public final class StringHelpers_jvmKt {
+ }
+
public final class TextFieldCursorKt {
}
@@ -690,6 +696,12 @@
public final class TextFieldGestureModifiersKt {
}
+ public final class TextFieldKeyInputKt {
+ }
+
+ public final class TextFieldKeyInput_androidKt {
+ }
+
public final class TextFieldPressGestureFilterKt {
}
@@ -741,6 +753,9 @@
public final class TextFieldSelectionManagerKt {
}
+ public final class TextFieldSelectionManager_androidKt {
+ }
+
@androidx.compose.runtime.Immutable public final class TextSelectionColors {
method public long getBackgroundColor-0d7_KjU();
method public long getHandleColor-0d7_KjU();
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 0fac70c..058fea0 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -591,9 +591,6 @@
package androidx.compose.foundation.text {
- public final class AndroidCoreTextField_androidKt {
- }
-
public final class BasicTextFieldKt {
method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
@@ -629,6 +626,12 @@
@kotlin.RequiresOptIn(message="Internal/Unstable API for use only between foundation modules sharing " + "the same exact version, subject to change without notice.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface InternalFoundationTextApi {
}
+ public final class KeyMappingKt {
+ }
+
+ public final class KeyMapping_androidKt {
+ }
+
public interface KeyboardActionScope {
method public void defaultKeyboardAction(androidx.compose.ui.text.input.ImeAction imeAction);
}
@@ -681,6 +684,9 @@
public final class MaxLinesHeightModifierKt {
}
+ public final class StringHelpers_jvmKt {
+ }
+
public final class TextFieldCursorKt {
}
@@ -690,6 +696,12 @@
public final class TextFieldGestureModifiersKt {
}
+ public final class TextFieldKeyInputKt {
+ }
+
+ public final class TextFieldKeyInput_androidKt {
+ }
+
public final class TextFieldPressGestureFilterKt {
}
@@ -741,6 +753,9 @@
public final class TextFieldSelectionManagerKt {
}
+ public final class TextFieldSelectionManager_androidKt {
+ }
+
@androidx.compose.runtime.Immutable public final class TextSelectionColors {
method public long getBackgroundColor-0d7_KjU();
method public long getHandleColor-0d7_KjU();
diff --git a/compose/foundation/foundation/api/restricted_1.0.0-beta02.txt b/compose/foundation/foundation/api/restricted_1.0.0-beta02.txt
index 132be90..76b8d5b 100644
--- a/compose/foundation/foundation/api/restricted_1.0.0-beta02.txt
+++ b/compose/foundation/foundation/api/restricted_1.0.0-beta02.txt
@@ -558,9 +558,6 @@
package androidx.compose.foundation.text {
- public final class AndroidCoreTextField_androidKt {
- }
-
public final class BasicTextFieldKt {
method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
@@ -593,6 +590,12 @@
method public static void appendInlineContent(androidx.compose.ui.text.AnnotatedString.Builder, String id, optional String alternateText);
}
+ public final class KeyMappingKt {
+ }
+
+ public final class KeyMapping_androidKt {
+ }
+
public interface KeyboardActionScope {
method public void defaultKeyboardAction(androidx.compose.ui.text.input.ImeAction imeAction);
}
@@ -645,6 +648,9 @@
public final class MaxLinesHeightModifierKt {
}
+ public final class StringHelpers_jvmKt {
+ }
+
public final class TextFieldCursorKt {
}
@@ -654,6 +660,12 @@
public final class TextFieldGestureModifiersKt {
}
+ public final class TextFieldKeyInputKt {
+ }
+
+ public final class TextFieldKeyInput_androidKt {
+ }
+
public final class TextFieldPressGestureFilterKt {
}
@@ -705,6 +717,9 @@
public final class TextFieldSelectionManagerKt {
}
+ public final class TextFieldSelectionManager_androidKt {
+ }
+
@androidx.compose.runtime.Immutable public final class TextSelectionColors {
method public long getBackgroundColor-0d7_KjU();
method public long getHandleColor-0d7_KjU();
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 132be90..76b8d5b 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -558,9 +558,6 @@
package androidx.compose.foundation.text {
- public final class AndroidCoreTextField_androidKt {
- }
-
public final class BasicTextFieldKt {
method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
@@ -593,6 +590,12 @@
method public static void appendInlineContent(androidx.compose.ui.text.AnnotatedString.Builder, String id, optional String alternateText);
}
+ public final class KeyMappingKt {
+ }
+
+ public final class KeyMapping_androidKt {
+ }
+
public interface KeyboardActionScope {
method public void defaultKeyboardAction(androidx.compose.ui.text.input.ImeAction imeAction);
}
@@ -645,6 +648,9 @@
public final class MaxLinesHeightModifierKt {
}
+ public final class StringHelpers_jvmKt {
+ }
+
public final class TextFieldCursorKt {
}
@@ -654,6 +660,12 @@
public final class TextFieldGestureModifiersKt {
}
+ public final class TextFieldKeyInputKt {
+ }
+
+ public final class TextFieldKeyInput_androidKt {
+ }
+
public final class TextFieldPressGestureFilterKt {
}
@@ -705,6 +717,9 @@
public final class TextFieldSelectionManagerKt {
}
+ public final class TextFieldSelectionManager_androidKt {
+ }
+
@androidx.compose.runtime.Immutable public final class TextSelectionColors {
method public long getBackgroundColor-0d7_KjU();
method public long getHandleColor-0d7_KjU();
diff --git a/compose/foundation/foundation/benchmark/build.gradle b/compose/foundation/foundation/benchmark/build.gradle
index 53d7b59..36498ab 100644
--- a/compose/foundation/foundation/benchmark/build.gradle
+++ b/compose/foundation/foundation/benchmark/build.gradle
@@ -35,7 +35,7 @@
androidTestImplementation project(":compose:foundation:foundation-layout")
androidTestImplementation project(":compose:foundation:foundation")
androidTestImplementation project(":compose:material:material")
- androidTestImplementation project(":compose:test-utils")
+ androidTestImplementation project(":compose:benchmark-utils")
androidTestImplementation(ANDROIDX_TEST_RULES)
androidTestImplementation(JUNIT)
androidTestImplementation(KOTLIN_STDLIB)
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/SimpleComponentImplementationBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/SimpleComponentImplementationBenchmark.kt
similarity index 67%
rename from compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/SimpleComponentImplementationBenchmark.kt
rename to compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/SimpleComponentImplementationBenchmark.kt
index 1c175f4..0e9330f 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/SimpleComponentImplementationBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/SimpleComponentImplementationBenchmark.kt
@@ -14,8 +14,22 @@
* limitations under the License.
*/
-package androidx.ui.benchmark.test
+package androidx.compose.foundation.benchmark
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.testutils.ComposeTestCase
+import androidx.compose.testutils.ToggleableTestCase
import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
import androidx.compose.testutils.benchmark.benchmarkDrawPerf
import androidx.compose.testutils.benchmark.benchmarkFirstCompose
@@ -27,14 +41,18 @@
import androidx.compose.testutils.benchmark.toggleStateBenchmarkLayout
import androidx.compose.testutils.benchmark.toggleStateBenchmarkMeasure
import androidx.compose.testutils.benchmark.toggleStateBenchmarkRecompose
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawOutline
+import androidx.compose.ui.graphics.drawscope.Stroke
+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 androidx.ui.integration.test.core.ComponentWithTwoLayoutNodesTestCase
-import androidx.ui.integration.test.core.ComponentWithRedrawTestCase
-import androidx.ui.integration.test.core.ComponentWithModifiersTestCase
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import androidx.test.ext.junit.runners.AndroidJUnit4
@LargeTest
@RunWith(AndroidJUnit4::class)
@@ -209,3 +227,77 @@
benchmarkRule.benchmarkDrawPerf(modifiersOnlyCaseFactory)
}
}
+
+class ComponentWithModifiersTestCase : SimpleComponentImplementationTestCase() {
+
+ @Composable
+ override fun Content() {
+ val innerSize = getInnerSize()
+ Box(
+ Modifier.size(48.dp)
+ .background(color = Color.Cyan)
+ .padding(innerSize.value)
+ .border(
+ color = Color.Cyan,
+ width = 1.dp,
+ shape = CircleShape
+ )
+ )
+ }
+}
+
+class ComponentWithRedrawTestCase : SimpleComponentImplementationTestCase() {
+
+ @Composable
+ override fun Content() {
+ val innerSize = getInnerSize()
+ val stroke = Stroke()
+ Canvas(Modifier.size(48.dp)) {
+ drawCircle(Color.Black, size.minDimension, style = stroke)
+ drawCircle(Color.Black, innerSize.value.value / 2f, center)
+ }
+ }
+}
+
+class ComponentWithTwoLayoutNodesTestCase : SimpleComponentImplementationTestCase() {
+ @Composable
+ override fun Content() {
+ Box(
+ modifier = Modifier
+ .size(48.dp)
+ .border(BorderStroke(1.dp, Color.Cyan), CircleShape)
+ .padding(1.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ val innerSize = getInnerSize().value
+ Canvas(Modifier.size(innerSize)) {
+ drawOutline(
+ CircleShape.createOutline(size, layoutDirection, this),
+ Color.Cyan
+ )
+ }
+ }
+ }
+}
+
+abstract class SimpleComponentImplementationTestCase : ComposeTestCase, ToggleableTestCase {
+
+ private var state: MutableState<Dp>? = null
+
+ @Composable
+ fun getInnerSize(): MutableState<Dp> {
+ val innerSize = remember { mutableStateOf(10.dp) }
+ state = innerSize
+ return innerSize
+ }
+
+ override fun toggleState() {
+ with(state!!) {
+ value = if (value == 10.dp) {
+ 20.dp
+ } else {
+ 10.dp
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/TrailingLambdaBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/TrailingLambdaBenchmark.kt
similarity index 98%
rename from compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/TrailingLambdaBenchmark.kt
rename to compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/TrailingLambdaBenchmark.kt
index cfa8133..335d283 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/TrailingLambdaBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/TrailingLambdaBenchmark.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.ui.benchmark.test
+package androidx.compose.foundation.benchmark
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index c96540c..e991a27 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -102,6 +102,8 @@
api("androidx.annotation:annotation:1.1.0")
}
+ desktopMain.dependsOn(jvmMain)
+
desktopMain.dependencies {
implementation(KOTLIN_STDLIB)
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/lint-baseline.xml b/compose/foundation/foundation/integration-tests/foundation-demos/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/foundation/foundation/integration-tests/foundation-demos/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/foundation/foundation/lint-baseline.xml b/compose/foundation/foundation/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/foundation/foundation/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/foundation/foundation/samples/lint-baseline.xml b/compose/foundation/foundation/samples/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/foundation/foundation/samples/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
new file mode 100644
index 0000000..db16b94
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
@@ -0,0 +1,299 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.textfield
+
+import android.view.KeyEvent
+import android.view.KeyEvent.META_CTRL_ON
+import android.view.KeyEvent.META_SHIFT_ON
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.nativeKeyCode
+import androidx.compose.ui.platform.LocalTextInputService
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.hasSetTextAction
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.performKeyPress
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.input.TextInputService
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import com.nhaarman.mockitokotlin2.mock
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalFoundationApi::class)
+class HardwareKeyboardTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun textField_typedEvents() {
+ keysSequenceTest {
+ Key.H.downAndUp()
+ Key.I.downAndUp(META_SHIFT_ON)
+ expectedText("hI")
+ }
+ }
+
+ @Test
+ fun textField_copyPaste() {
+ keysSequenceTest(initText = "hello") {
+ Key.A.downAndUp(META_CTRL_ON)
+ Key.C.downAndUp(META_CTRL_ON)
+ Key.DirectionRight.downAndUp()
+ Key.Spacebar.downAndUp()
+ Key.V.downAndUp(META_CTRL_ON)
+ expectedText("hello hello")
+ }
+ }
+
+ @Test
+ fun textField_linesNavigation() {
+ keysSequenceTest(initText = "hello\nworld") {
+ Key.DirectionDown.downAndUp()
+ Key.Zero.downAndUp()
+ Key.DirectionUp.downAndUp()
+ Key.Zero.downAndUp()
+ expectedText("h0ello\n0world")
+ Key.DirectionUp.downAndUp()
+ Key.Zero.downAndUp()
+ expectedText("0h0ello\n0world")
+ }
+ }
+
+ @Test
+ fun textField_newLine() {
+ keysSequenceTest(initText = "hello") {
+ Key.Enter.downAndUp()
+ expectedText("\nhello")
+ }
+ }
+
+ @Test
+ fun textField_backspace() {
+ keysSequenceTest(initText = "hello") {
+ Key.DirectionRight.downAndUp()
+ Key.DirectionRight.downAndUp()
+ Key.Backspace.downAndUp()
+ expectedText("hllo")
+ }
+ }
+
+ @Test
+ fun textField_delete() {
+ keysSequenceTest(initText = "hello") {
+ Key.Delete.downAndUp()
+ expectedText("ello")
+ }
+ }
+
+ @Test
+ fun textField_nextWord() {
+ keysSequenceTest(initText = "hello world") {
+ Key.DirectionRight.downAndUp(META_CTRL_ON)
+ Key.Zero.downAndUp()
+ expectedText("hello0 world")
+ Key.DirectionRight.downAndUp(META_CTRL_ON)
+ Key.Zero.downAndUp()
+ expectedText("hello0 world0")
+ }
+ }
+
+ @Test
+ fun textField_nextWord_doubleSpace() {
+ keysSequenceTest(initText = "hello world") {
+ Key.DirectionRight.downAndUp(META_CTRL_ON)
+ Key.DirectionRight.downAndUp()
+ Key.DirectionRight.downAndUp(META_CTRL_ON)
+ Key.Zero.downAndUp()
+ expectedText("hello world0")
+ }
+ }
+
+ @Test
+ fun textField_prevWord() {
+ keysSequenceTest(initText = "hello world") {
+ Key.DirectionRight.downAndUp(META_CTRL_ON)
+ Key.DirectionRight.downAndUp(META_CTRL_ON)
+ Key.DirectionLeft.downAndUp(META_CTRL_ON)
+ Key.Zero.downAndUp()
+ expectedText("hello 0world")
+ }
+ }
+
+ @Test
+ fun textField_HomeAndEnd() {
+ keysSequenceTest(initText = "hello world") {
+ Key.MoveEnd.downAndUp()
+ Key.Zero.downAndUp()
+ Key.MoveHome.downAndUp()
+ Key.Zero.downAndUp()
+ expectedText("0hello world0")
+ }
+ }
+
+ @Test
+ fun textField_byWordSelection() {
+ keysSequenceTest(initText = "hello world\nhi") {
+ Key.DirectionRight.downAndUp(META_SHIFT_ON or META_CTRL_ON)
+ expectedSelection(TextRange(0, 5))
+ Key.DirectionRight.downAndUp(META_SHIFT_ON or META_CTRL_ON)
+ expectedSelection(TextRange(0, 12))
+ Key.DirectionRight.downAndUp(META_SHIFT_ON or META_CTRL_ON)
+ expectedSelection(TextRange(0, 15))
+ Key.DirectionLeft.downAndUp(META_SHIFT_ON or META_CTRL_ON)
+ expectedSelection(TextRange(0, 13))
+ }
+ }
+
+ @Test
+ fun textField_lineEndStart() {
+ keysSequenceTest(initText = "hello world\nhi") {
+ Key.MoveEnd.downAndUp()
+ Key.Zero.downAndUp()
+ expectedText("hello world0\nhi")
+ Key.MoveEnd.downAndUp()
+ Key.MoveHome.downAndUp()
+ Key.Zero.downAndUp()
+ expectedText("0hello world0\nhi")
+ Key.MoveEnd.downAndUp(META_SHIFT_ON)
+ expectedSelection(TextRange(1, 16))
+ }
+ }
+
+ @Test
+ fun textField_deleteWords() {
+ keysSequenceTest(initText = "hello world\nhi world") {
+ Key.MoveEnd.downAndUp()
+ Key.Backspace.downAndUp(META_CTRL_ON)
+ expectedText("hello \nhi world")
+ Key.Delete.downAndUp(META_CTRL_ON)
+ expectedText("hello world")
+ }
+ }
+
+ @Test
+ fun textField_paragraphNavigation() {
+ keysSequenceTest(initText = "hello world\nhi") {
+ Key.DirectionDown.downAndUp(META_CTRL_ON)
+ Key.Zero.downAndUp()
+ expectedText("hello world0\nhi")
+ Key.DirectionDown.downAndUp(META_CTRL_ON)
+ Key.DirectionUp.downAndUp(META_CTRL_ON)
+ Key.Zero.downAndUp()
+ expectedText("hello world0\n0hi")
+ }
+ }
+
+ @Test
+ fun textField_selectionCaret() {
+ keysSequenceTest(initText = "hello world") {
+ Key.DirectionRight.downAndUp(META_CTRL_ON or META_SHIFT_ON)
+ expectedSelection(TextRange(0, 5))
+ Key.DirectionRight.downAndUp(META_SHIFT_ON)
+ expectedSelection(TextRange(0, 6))
+ Key.Backslash.downAndUp(META_CTRL_ON)
+ expectedSelection(TextRange(6, 6))
+ Key.DirectionLeft.downAndUp(META_CTRL_ON or META_SHIFT_ON)
+ expectedSelection(TextRange(6, 0))
+ Key.DirectionRight.downAndUp(META_SHIFT_ON)
+ expectedSelection(TextRange(6, 1))
+ }
+ }
+
+ private inner class SequenceScope(
+ val state: MutableState<TextFieldValue>,
+ val nodeGetter: () -> SemanticsNodeInteraction
+ ) {
+ fun Key.downAndUp(metaState: Int = 0) {
+ this.down(metaState)
+ this.up(metaState)
+ }
+
+ fun Key.down(metaState: Int = 0) {
+ nodeGetter().performKeyPress(downEvent(this, metaState))
+ }
+
+ fun Key.up(metaState: Int = 0) {
+ nodeGetter().performKeyPress(upEvent(this, metaState))
+ }
+
+ fun expectedText(text: String) {
+ rule.runOnIdle {
+ Truth.assertThat(state.value.text).isEqualTo(text)
+ }
+ }
+
+ fun expectedSelection(selection: TextRange) {
+ rule.runOnIdle {
+ Truth.assertThat(state.value.selection).isEqualTo(selection)
+ }
+ }
+ }
+
+ private fun keysSequenceTest(
+ initText: String = "",
+ sequence: SequenceScope.() -> Unit
+ ) {
+ val inputService = TextInputService(mock())
+
+ val state = mutableStateOf(TextFieldValue(initText))
+ val focusFequester = FocusRequester()
+ rule.setContent {
+ CompositionLocalProvider(
+ LocalTextInputService provides inputService
+ ) {
+ BasicTextField(
+ value = state.value,
+ modifier = Modifier.fillMaxSize().focusRequester(focusFequester),
+ onValueChange = {
+ state.value = it
+ }
+ )
+ }
+ }
+
+ rule.runOnIdle { focusFequester.requestFocus() }
+
+ sequence(SequenceScope(state) { rule.onNode(hasSetTextAction()) })
+ }
+}
+
+private fun downEvent(key: Key, metaState: Int = 0): androidx.compose.ui.input.key.KeyEvent {
+ return androidx.compose.ui.input.key.KeyEvent(
+ KeyEvent(0L, 0L, KeyEvent.ACTION_DOWN, key.nativeKeyCode, 0, metaState)
+ )
+}
+
+private fun upEvent(key: Key, metaState: Int = 0): androidx.compose.ui.input.key.KeyEvent {
+ return androidx.compose.ui.input.key.KeyEvent(
+ KeyEvent(0L, 0L, KeyEvent.ACTION_UP, key.nativeKeyCode, 0, metaState)
+ )
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/KeyMapping.android.kt
similarity index 79%
rename from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
rename to compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/KeyMapping.android.kt
index 7e01354..c4984e3 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/KeyMapping.android.kt
@@ -1,5 +1,3 @@
-// ktlint-disable filename
-
/*
* Copyright 2021 The Android Open Source Project
*
@@ -16,6 +14,6 @@
* limitations under the License.
*/
-package androidx.compose.foundation
+package androidx.compose.foundation.text
-internal actual typealias AtomicReference<V> = java.util.concurrent.atomic.AtomicReference<V>
\ No newline at end of file
+internal actual val platformDefaultKeyMapping = defaultKeyMapping
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/AndroidCoreTextField.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.android.kt
similarity index 73%
rename from compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/AndroidCoreTextField.android.kt
rename to compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.android.kt
index 6a9e70d..f5f3a55 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/AndroidCoreTextField.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.android.kt
@@ -16,9 +16,8 @@
package androidx.compose.foundation.text
-import androidx.compose.foundation.text.selection.TextFieldSelectionManager
-import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.KeyEvent
-internal actual fun Modifier.textFieldKeyboardModifier(
- manager: TextFieldSelectionManager
-): Modifier = this.then(Modifier)
\ No newline at end of file
+internal actual val KeyEvent.isTypedEvent: Boolean
+ get() = nativeKeyEvent.action == android.view.KeyEvent.ACTION_DOWN &&
+ nativeKeyEvent.unicodeChar != 0
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.android.kt
similarity index 76%
copy from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
copy to compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.android.kt
index 7e01354..16182e9 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.android.kt
@@ -1,5 +1,3 @@
-// ktlint-disable filename
-
/*
* Copyright 2021 The Android Open Source Project
*
@@ -16,6 +14,9 @@
* limitations under the License.
*/
-package androidx.compose.foundation
+package androidx.compose.foundation.text.selection
-internal actual typealias AtomicReference<V> = java.util.concurrent.atomic.AtomicReference<V>
\ No newline at end of file
+import androidx.compose.ui.input.pointer.PointerEvent
+
+internal actual val PointerEvent.isShiftPressed: Boolean
+ get() = false
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 14de931..58f75c4 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
@@ -299,10 +299,7 @@
.then(selectionModifier)
.then(focusRequestTapModifier)
} else {
- Modifier.mouseDragGestureFilter(
- manager.mouseSelectionObserver(onStart = { focusRequester.requestFocus() }),
- enabled = enabled
- )
+ Modifier.mouseDragGestureDetector(manager::mouseSelectionDetector, enabled = enabled)
}
val drawModifier = Modifier.drawBehind {
@@ -432,6 +429,16 @@
onDispose { manager.hideSelectionToolbar() }
}
+ val textKeyInputModifier =
+ Modifier.textFieldKeyInput(
+ state = state,
+ manager = manager,
+ value = value,
+ editable = !readOnly,
+ singleLine = maxLines == 1,
+ offsetMapping = offsetMapping
+ )
+
// Modifiers that should be applied to the outer text field container. Usually those include
// gesture and semantics modifiers.
val decorationBoxModifier = modifier
@@ -439,6 +446,7 @@
.then(pointerModifier)
.then(semanticsModifier)
.then(focusModifier)
+ .then(textKeyInputModifier)
.onGloballyPositioned {
state.layoutResult?.decorationBoxCoordinates = it
}
@@ -459,7 +467,6 @@
.then(drawModifier)
.then(onPositionedModifier)
.textFieldMinSize(textStyle)
- .textFieldKeyboardModifier(manager)
SimpleLayout(coreTextFieldModifier) {
Layout({ }) { _, constraints ->
@@ -498,8 +505,6 @@
}
}
-internal expect fun Modifier.textFieldKeyboardModifier(manager: TextFieldSelectionManager): Modifier
-
@OptIn(InternalFoundationTextApi::class)
internal class TextFieldState(
var textDelegate: TextDelegate
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyCommand.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyCommand.kt
new file mode 100644
index 0000000..3b76844
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyCommand.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+internal enum class KeyCommand(
+ // Indicates, that this command is supposed to edit text so should be applied only to
+ // editable text fields
+ val editsText: Boolean
+) {
+ LEFT_CHAR(false),
+ RIGHT_CHAR(false),
+
+ RIGHT_WORD(false),
+ LEFT_WORD(false),
+
+ NEXT_PARAGRAPH(false),
+ PREV_PARAGRAPH(false),
+
+ LINE_START(false),
+ LINE_END(false),
+ LINE_LEFT(false),
+ LINE_RIGHT(false),
+
+ UP(false),
+ DOWN(false),
+
+ PAGE_UP(false),
+ PAGE_DOWN(false),
+
+ HOME(false),
+ END(false),
+
+ COPY(false),
+ PASTE(true),
+ CUT(true),
+
+ DELETE_PREV_CHAR(true),
+ DELETE_NEXT_CHAR(true),
+
+ DELETE_PREV_WORD(true),
+ DELETE_NEXT_WORD(true),
+
+ DELETE_FROM_LINE_START(true),
+ DELETE_TO_LINE_END(true),
+
+ SELECT_ALL(false),
+
+ SELECT_LEFT_CHAR(false),
+ SELECT_RIGHT_CHAR(false),
+
+ SELECT_UP(false),
+ SELECT_DOWN(false),
+
+ SELECT_PAGE_UP(false),
+ SELECT_PAGE_DOWN(false),
+
+ SELECT_HOME(false),
+ SELECT_END(false),
+
+ SELECT_LEFT_WORD(false),
+ SELECT_RIGHT_WORD(false),
+ SELECT_NEXT_PARAGRAPH(false),
+ SELECT_PREV_PARAGRAPH(false),
+
+ SELECT_LINE_START(false),
+ SELECT_LINE_END(false),
+ SELECT_LINE_LEFT(false),
+ SELECT_LINE_RIGHT(false),
+
+ DESELECT(false),
+
+ NEW_LINE(true),
+ TAB(true)
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyMapping.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyMapping.kt
new file mode 100644
index 0000000..5d393a5
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyMapping.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.isCtrlPressed
+import androidx.compose.ui.input.key.isShiftPressed
+import androidx.compose.ui.input.key.key
+
+internal interface KeyMapping {
+ fun map(event: KeyEvent): KeyCommand?
+}
+
+// each platform can define its own key mapping, on Android its just defaultKeyMapping, but on
+// desktop, the value depends on the current OS
+internal expect val platformDefaultKeyMapping: KeyMapping
+
+// It's common for all platforms key mapping
+internal fun commonKeyMapping(
+ shortcutModifier: (KeyEvent) -> Boolean
+): KeyMapping {
+ return object : KeyMapping {
+ override fun map(event: KeyEvent): KeyCommand? {
+ return when {
+ shortcutModifier(event) ->
+ when (event.key) {
+ Key.C, Key.Insert -> KeyCommand.COPY
+ Key.V -> KeyCommand.PASTE
+ Key.X -> KeyCommand.CUT
+ Key.A -> KeyCommand.SELECT_ALL
+ else -> null
+ }
+ event.isCtrlPressed -> null
+ event.isShiftPressed ->
+ when (event.key) {
+ Key.DirectionLeft -> KeyCommand.SELECT_LEFT_CHAR
+ Key.DirectionRight -> KeyCommand.SELECT_RIGHT_CHAR
+ Key.DirectionUp -> KeyCommand.SELECT_UP
+ Key.DirectionDown -> KeyCommand.SELECT_DOWN
+ Key.PageUp -> KeyCommand.SELECT_PAGE_UP
+ Key.PageDown -> KeyCommand.SELECT_PAGE_DOWN
+ Key.MoveHome -> KeyCommand.SELECT_LINE_START
+ Key.MoveEnd -> KeyCommand.SELECT_LINE_END
+ Key.Insert -> KeyCommand.PASTE
+ else -> null
+ }
+ else ->
+ when (event.key) {
+ Key.DirectionLeft -> KeyCommand.LEFT_CHAR
+ Key.DirectionRight -> KeyCommand.RIGHT_CHAR
+ Key.DirectionUp -> KeyCommand.UP
+ Key.DirectionDown -> KeyCommand.DOWN
+ Key.PageUp -> KeyCommand.PAGE_UP
+ Key.PageDown -> KeyCommand.PAGE_DOWN
+ Key.MoveHome -> KeyCommand.LINE_START
+ Key.MoveEnd -> KeyCommand.LINE_END
+ Key.Enter -> KeyCommand.NEW_LINE
+ Key.Backspace -> KeyCommand.DELETE_PREV_CHAR
+ Key.Delete -> KeyCommand.DELETE_NEXT_CHAR
+ Key.Paste -> KeyCommand.PASTE
+ Key.Cut -> KeyCommand.CUT
+ Key.Tab -> KeyCommand.TAB
+ else -> null
+ }
+ }
+ }
+ }
+}
+
+// It's "default" or actually "non macOS" key mapping
+internal val defaultKeyMapping: KeyMapping =
+ commonKeyMapping(KeyEvent::isCtrlPressed).let { common ->
+ object : KeyMapping {
+ override fun map(event: KeyEvent): KeyCommand? {
+ return when {
+ event.isShiftPressed && event.isCtrlPressed ->
+ when (event.key) {
+ Key.DirectionLeft -> KeyCommand.SELECT_LEFT_WORD
+ Key.DirectionRight -> KeyCommand.SELECT_RIGHT_WORD
+ Key.DirectionUp -> KeyCommand.SELECT_PREV_PARAGRAPH
+ Key.DirectionDown -> KeyCommand.SELECT_NEXT_PARAGRAPH
+ else -> null
+ }
+ event.isCtrlPressed ->
+ when (event.key) {
+ Key.DirectionLeft -> KeyCommand.LEFT_WORD
+ Key.DirectionRight -> KeyCommand.RIGHT_WORD
+ Key.DirectionUp -> KeyCommand.PREV_PARAGRAPH
+ Key.DirectionDown -> KeyCommand.NEXT_PARAGRAPH
+ Key.H -> KeyCommand.DELETE_PREV_CHAR
+ Key.Delete -> KeyCommand.DELETE_NEXT_WORD
+ Key.Backspace -> KeyCommand.DELETE_PREV_WORD
+ Key.Backslash -> KeyCommand.DESELECT
+ else -> null
+ }
+ event.isShiftPressed ->
+ when (event.key) {
+ Key.MoveHome -> KeyCommand.SELECT_HOME
+ Key.MoveEnd -> KeyCommand.SELECT_END
+ else -> null
+ }
+ else -> null
+ } ?: common.map(event)
+ }
+ }
+ }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/StringHelpers.kt
similarity index 64%
copy from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
copy to compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/StringHelpers.kt
index 7e01354..dbbf9df 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/StringHelpers.kt
@@ -1,5 +1,3 @@
-// ktlint-disable filename
-
/*
* Copyright 2021 The Android Open Source Project
*
@@ -16,6 +14,11 @@
* limitations under the License.
*/
-package androidx.compose.foundation
+package androidx.compose.foundation.text
-internal actual typealias AtomicReference<V> = java.util.concurrent.atomic.AtomicReference<V>
\ No newline at end of file
+// StringBuilder.appendCodePoint is already defined on JVM so it's called appendCodePointX
+internal expect fun StringBuilder.appendCodePointX(codePoint: Int): StringBuilder
+
+internal expect fun String.findPrecedingBreak(index: Int): Int
+
+internal expect fun String.findFollowingBreak(index: Int): Int
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldGestureModifiers.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldGestureModifiers.kt
index af50a91..b1f6e32 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldGestureModifiers.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldGestureModifiers.kt
@@ -31,6 +31,8 @@
import androidx.compose.foundation.legacygestures.dragGestureFilter
import androidx.compose.foundation.legacygestures.longPressDragGestureFilter
import androidx.compose.foundation.legacygestures.tapGestureFilter
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.pointerInput
// Touch selection
internal fun Modifier.longPressDragGestureFilter(
@@ -56,4 +58,9 @@
internal fun Modifier.mouseDragGestureFilter(
dragObserver: DragObserver,
enabled: Boolean
-) = if (enabled) this.dragGestureFilter(dragObserver, startDragImmediately = true) else this
\ No newline at end of file
+) = if (enabled) this.dragGestureFilter(dragObserver, startDragImmediately = true) else this
+
+internal fun Modifier.mouseDragGestureDetector(
+ detector: suspend PointerInputScope.() -> Unit,
+ enabled: Boolean
+) = if (enabled) Modifier.pointerInput(Unit, detector) else this
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt
new file mode 100644
index 0000000..b792da1
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.foundation.text.selection.TextFieldPreparedSelection
+import androidx.compose.foundation.text.selection.TextFieldSelectionManager
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.KeyEventType
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.input.key.type
+import androidx.compose.ui.input.key.utf16CodePoint
+import androidx.compose.ui.text.input.CommitTextCommand
+import androidx.compose.ui.text.input.EditCommand
+import androidx.compose.ui.text.input.OffsetMapping
+import androidx.compose.ui.text.input.TextFieldValue
+
+// AWT and Android have similar but different key event models. In android there are two main
+// types of events: ACTION_DOWN and ACTION_UP. In AWT there is additional KEY_TYPED which should
+// be used to get "typed character". By this simple function we are introducing common
+// denominator for both systems: if KeyEvent.isTypedEvent then it's safe to use
+// KeyEvent.utf16CodePoint
+internal expect val KeyEvent.isTypedEvent: Boolean
+
+/**
+ * It handles [KeyEvent]s and either process them as typed events or maps to
+ * [KeyCommand] via [KeyMapping]. [KeyCommand] then is executed
+ * using utility class [TextFieldPreparedSelection]
+ */
+internal class TextFieldKeyInput(
+ val state: TextFieldState,
+ val selectionManager: TextFieldSelectionManager,
+ val value: TextFieldValue = TextFieldValue(),
+ val editable: Boolean = true,
+ val singleLine: Boolean = false,
+ val offsetMapping: OffsetMapping = OffsetMapping.Identity,
+ private val keyMapping: KeyMapping = platformDefaultKeyMapping,
+) {
+ private fun EditCommand.apply() {
+ state.onValueChange(state.processor.apply(listOf(this)))
+ }
+
+ private fun typedCommand(event: KeyEvent): CommitTextCommand? =
+ if (event.isTypedEvent) {
+ val text = StringBuilder().appendCodePointX(event.utf16CodePoint)
+ .toString()
+ CommitTextCommand(text, 1)
+ } else {
+ null
+ }
+
+ fun process(event: KeyEvent): Boolean {
+ typedCommand(event)?.let {
+ return if (editable) {
+ it.apply()
+ true
+ } else {
+ false
+ }
+ }
+ if (event.type != KeyEventType.KeyDown) {
+ return false
+ }
+ val command = keyMapping.map(event)
+ if (command == null || (command.editsText && !editable)) {
+ return false
+ }
+ var consumed = true
+ commandExecutionContext {
+ when (command) {
+ KeyCommand.COPY -> selectionManager.copy(false)
+ KeyCommand.PASTE -> selectionManager.paste()
+ KeyCommand.CUT -> selectionManager.cut()
+ KeyCommand.LEFT_CHAR -> collapseLeftOr { moveCursorLeft() }
+ KeyCommand.RIGHT_CHAR -> collapseRightOr { moveCursorRight() }
+ KeyCommand.LEFT_WORD -> moveCursorLeftByWord()
+ KeyCommand.RIGHT_WORD -> moveCursorRightByWord()
+ KeyCommand.PREV_PARAGRAPH -> moveCursorPrevByParagraph()
+ KeyCommand.NEXT_PARAGRAPH -> moveCursorNextByParagraph()
+ KeyCommand.UP -> moveCursorUpByLine()
+ KeyCommand.DOWN -> moveCursorDownByLine()
+ KeyCommand.PAGE_UP -> moveCursorUpByPage()
+ KeyCommand.PAGE_DOWN -> moveCursorDownByPage()
+ KeyCommand.LINE_START -> moveCursorToLineStart()
+ KeyCommand.LINE_END -> moveCursorToLineEnd()
+ KeyCommand.LINE_LEFT -> moveCursorToLineLeftSide()
+ KeyCommand.LINE_RIGHT -> moveCursorToLineRightSide()
+ KeyCommand.HOME -> moveCursorToHome()
+ KeyCommand.END -> moveCursorToEnd()
+ KeyCommand.DELETE_PREV_CHAR ->
+ deleteIfSelectedOr {
+ moveCursorPrev().selectMovement().deleteSelected()
+ }
+ KeyCommand.DELETE_NEXT_CHAR -> {
+ deleteIfSelectedOr {
+ moveCursorNext().selectMovement().deleteSelected()
+ }
+ }
+ KeyCommand.DELETE_PREV_WORD ->
+ deleteIfSelectedOr {
+ moveCursorPrevByWord().selectMovement().deleteSelected()
+ }
+ KeyCommand.DELETE_NEXT_WORD ->
+ deleteIfSelectedOr {
+ moveCursorNextByWord().selectMovement().deleteSelected()
+ }
+ KeyCommand.DELETE_FROM_LINE_START ->
+ deleteIfSelectedOr {
+ moveCursorToLineStart().selectMovement().deleteSelected()
+ }
+ KeyCommand.DELETE_TO_LINE_END ->
+ deleteIfSelectedOr {
+ moveCursorToLineEnd().selectMovement().deleteSelected()
+ }
+ KeyCommand.NEW_LINE ->
+ if (!singleLine) {
+ CommitTextCommand("\n", 1).apply()
+ } else {
+ consumed = false
+ }
+ KeyCommand.TAB ->
+ if (!singleLine) {
+ CommitTextCommand("\t", 1).apply()
+ } else {
+ consumed = false
+ }
+ KeyCommand.SELECT_ALL -> selectAll()
+ KeyCommand.SELECT_LEFT_CHAR -> moveCursorLeft().selectMovement()
+ KeyCommand.SELECT_RIGHT_CHAR -> moveCursorRight().selectMovement()
+ KeyCommand.SELECT_LEFT_WORD -> moveCursorLeftByWord().selectMovement()
+ KeyCommand.SELECT_RIGHT_WORD -> moveCursorRightByWord().selectMovement()
+ KeyCommand.SELECT_PREV_PARAGRAPH -> moveCursorPrevByParagraph().selectMovement()
+ KeyCommand.SELECT_NEXT_PARAGRAPH -> moveCursorNextByParagraph().selectMovement()
+ KeyCommand.SELECT_LINE_START -> moveCursorToLineStart().selectMovement()
+ KeyCommand.SELECT_LINE_END -> moveCursorToLineEnd().selectMovement()
+ KeyCommand.SELECT_LINE_LEFT -> moveCursorToLineLeftSide().selectMovement()
+ KeyCommand.SELECT_LINE_RIGHT -> moveCursorToLineRightSide().selectMovement()
+ KeyCommand.SELECT_UP -> moveCursorUpByLine().selectMovement()
+ KeyCommand.SELECT_DOWN -> moveCursorDownByLine().selectMovement()
+ KeyCommand.SELECT_PAGE_UP -> moveCursorUpByPage().selectMovement()
+ KeyCommand.SELECT_PAGE_DOWN -> moveCursorDownByPage().selectMovement()
+ KeyCommand.SELECT_HOME -> moveCursorToHome().selectMovement()
+ KeyCommand.SELECT_END -> moveCursorToEnd().selectMovement()
+ KeyCommand.DESELECT -> deselect()
+ }
+ }
+ return consumed
+ }
+
+ private fun commandExecutionContext(block: TextFieldPreparedSelection.() -> Unit) {
+ val preparedSelection = TextFieldPreparedSelection(
+ currentValue = value,
+ offsetMapping = offsetMapping,
+ layoutResultProxy = state.layoutResult
+ )
+ block(preparedSelection)
+ if (preparedSelection.selection != value.selection ||
+ preparedSelection.annotatedString != value.annotatedString
+ ) {
+ state.onValueChange(preparedSelection.value)
+ }
+ }
+}
+
+internal fun Modifier.textFieldKeyInput(
+ state: TextFieldState,
+ manager: TextFieldSelectionManager,
+ value: TextFieldValue,
+ editable: Boolean,
+ singleLine: Boolean,
+ offsetMapping: OffsetMapping
+): Modifier {
+ val processor = TextFieldKeyInput(
+ state = state,
+ selectionManager = manager,
+ value = value,
+ editable = editable,
+ singleLine = singleLine,
+ offsetMapping = offsetMapping
+ )
+ return Modifier.onKeyEvent(processor::process)
+}
\ No newline at end of file
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 e8c775c..55616a6 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
@@ -18,6 +18,8 @@
package androidx.compose.foundation.text.selection
+import androidx.compose.foundation.gestures.drag
+import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.foundation.text.TextFieldDelegate
import androidx.compose.foundation.text.TextFieldState
import androidx.compose.runtime.Composable
@@ -34,6 +36,14 @@
import androidx.compose.foundation.text.InternalFoundationTextApi
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.input.pointer.AwaitPointerEventScope
+import androidx.compose.ui.input.pointer.PointerEvent
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.PointerType
+import androidx.compose.ui.input.pointer.changedToDown
+import androidx.compose.ui.input.pointer.consumeAllChanges
+import androidx.compose.ui.input.pointer.consumeDownChange
+import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.TextToolbarStatus
@@ -48,6 +58,7 @@
import androidx.compose.ui.text.input.getTextBeforeSelection
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastAll
import kotlin.math.max
import kotlin.math.min
@@ -215,46 +226,81 @@
}
}
- internal fun mouseSelectionObserver(onStart: (Offset) -> Unit) = object : DragObserver {
- override fun onStart(downPosition: Offset) {
- onStart(downPosition)
+ internal interface MouseSelectionObserver {
+ fun onDown(downPosition: Offset, shiftIsPressed: Boolean)
+ fun onDrag(dragDistance: Offset)
+ }
- state?.layoutResult?.let { layoutResult ->
- TextFieldDelegate.setCursorOffset(
- downPosition,
- layoutResult,
- state!!.processor,
- offsetMapping,
- onValueChange
- )
- dragBeginOffsetInText = layoutResult.getOffsetForPosition(downPosition)
- }
+ internal val mouseSelectionObserver = object : MouseSelectionObserver {
+ override fun onDown(downPosition: Offset, shiftIsPressed: Boolean) {
+ focusRequester?.requestFocus()
dragBeginPosition = downPosition
dragTotalDistance = Offset.Zero
- }
- override fun onDrag(dragDistance: Offset): Offset {
- // selection never started, did not consume any drag
- if (value.text.isEmpty()) return Offset.Zero
+ state?.layoutResult?.let { layoutResult ->
+ dragBeginOffsetInText = layoutResult.getOffsetForPosition(downPosition)
+ // * Without shift it starts the new selection from the scratch.
+ // * With shift expand / shrink existed selection.
+ // * Click sets start and end of the selection, but shift click only the end of
+ // selection.
+ // * The specific case of it when selection is collapsed, but the same logic is
+ // applied for not collapsed selection too.
+ if (shiftIsPressed) {
+ val startOffset = offsetMapping.originalToTransformed(value.selection.start)
+ val clickOffset = layoutResult.getOffsetForPosition(dragBeginPosition)
+ updateSelection(
+ value = value,
+ transformedStartOffset = startOffset,
+ transformedEndOffset = clickOffset,
+ isStartHandle = false,
+ wordBasedSelection = false
+ )
+ } else {
+ TextFieldDelegate.setCursorOffset(
+ downPosition,
+ layoutResult,
+ state!!.processor,
+ offsetMapping,
+ onValueChange
+ )
+ }
+ }
+ }
+ override fun onDrag(dragDistance: Offset) {
+ if (value.text.isEmpty()) return
dragTotalDistance += dragDistance
state?.layoutResult?.let { layoutResult ->
- val startOffset = dragBeginOffsetInText ?: layoutResult
- .getOffsetForPosition(dragBeginPosition, false)
- val endOffset = layoutResult.getOffsetForPosition(
- position = dragBeginPosition + dragTotalDistance,
- coerceInVisibleBounds = false
- )
+ val dragOffset =
+ layoutResult.getOffsetForPosition(
+ position = dragBeginPosition + dragTotalDistance,
+ coerceInVisibleBounds = false
+ )
+ val startOffset = offsetMapping.originalToTransformed(value.selection.start)
updateSelection(
value = value,
transformedStartOffset = startOffset,
- transformedEndOffset = endOffset,
+ transformedEndOffset = dragOffset,
isStartHandle = false,
wordBasedSelection = false
)
}
- return dragDistance
+ }
+ }
+
+ internal suspend fun mouseSelectionDetector(scope: PointerInputScope) {
+ scope.forEachGesture {
+ awaitPointerEventScope {
+ val down = awaitMouseEventFirstDown()
+ val downChange = down.changes[0]
+ downChange.consumeDownChange()
+ mouseSelectionObserver.onDown(downChange.position, down.isShiftPressed)
+ drag(downChange.id) {
+ mouseSelectionObserver.onDrag(it.positionChange())
+ it.consumeAllChanges()
+ }
+ }
}
}
@@ -643,3 +689,16 @@
): Boolean = state?.layoutCoordinates?.visibleBounds()?.containsInclusive(
getHandlePosition(isStartHandle)
) ?: false
+
+private suspend fun AwaitPointerEventScope.awaitMouseEventFirstDown(): PointerEvent {
+ var event: PointerEvent
+ do {
+ event = awaitPointerEvent()
+ } while (
+ !event.changes.fastAll { it.type == PointerType.Mouse && it.changedToDown() }
+ )
+ return event
+}
+
+// TODO(b/180075467) it should be part of PointerEvent API in one way or another
+internal expect val PointerEvent.isShiftPressed: Boolean
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextPreparedSelection.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextPreparedSelection.kt
new file mode 100644
index 0000000..68d1712
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextPreparedSelection.kt
@@ -0,0 +1,394 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text.selection
+
+import androidx.compose.foundation.text.TextLayoutResultProxy
+import androidx.compose.foundation.text.findFollowingBreak
+import androidx.compose.foundation.text.findPrecedingBreak
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.OffsetMapping
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.style.ResolvedTextDirection
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * This utility class implements many selection-related operations on text (including basic
+ * cursor movements and deletions) and combines them, taking into account how the text was
+ * rendered. So, for example, [moveCursorToLineEnd] moves it to the visual line end.
+ *
+ * For many of these operations, it's particularly important to keep the difference between
+ * selection start and selection end. In some systems, they are called "anchor" and "caret"
+ * respectively. For example, for selection from scratch, after [moveCursorLeftByWord]
+ * [moveCursorRight] will move the left side of the selection, but after [moveCursorRightByWord]
+ * the right one.
+ *
+ * To use it in scope of text fields see [TextFieldPreparedSelection]
+ */
+internal abstract class BaseTextPreparedSelection<T : BaseTextPreparedSelection<T>>(
+ val originalText: AnnotatedString,
+ val originalSelection: TextRange,
+ val layoutResult: TextLayoutResult?,
+ val offsetMapping: OffsetMapping
+) {
+ var selection = originalSelection
+
+ var annotatedString = originalText
+ protected val text
+ get() = annotatedString.text
+
+ @Suppress("UNCHECKED_CAST")
+ inline fun <U> U.apply(block: U.() -> Unit): T {
+ block()
+ return this as T
+ }
+
+ fun setCursor(offset: Int) = apply {
+ setSelection(offset, offset)
+ }
+
+ fun setSelection(start: Int, end: Int) = apply {
+ selection = TextRange(start, end)
+ }
+
+ fun selectAll() = apply {
+ setSelection(0, text.length)
+ }
+
+ fun deselect() = apply {
+ setCursor(selection.end)
+ }
+
+ fun moveCursorLeft() = apply {
+ if (isLtr()) {
+ moveCursorPrev()
+ } else {
+ moveCursorNext()
+ }
+ }
+
+ fun moveCursorRight() = apply {
+ if (isLtr()) {
+ moveCursorNext()
+ } else {
+ moveCursorPrev()
+ }
+ }
+
+ /**
+ * If there is already a selection, collapse it to the left side. Otherwise, execute [or]
+ */
+ fun collapseLeftOr(or: T.() -> Unit) = apply {
+ if (selection.collapsed) {
+ @Suppress("UNCHECKED_CAST")
+ or(this as T)
+ } else {
+ if (isLtr()) {
+ setCursor(selection.min)
+ } else {
+ setCursor(selection.max)
+ }
+ }
+ }
+
+ /**
+ * If there is already a selection, collapse it to the right side. Otherwise, execute [or]
+ */
+ fun collapseRightOr(or: T.() -> Unit) = apply {
+ if (selection.collapsed) {
+ @Suppress("UNCHECKED_CAST")
+ or(this as T)
+ } else {
+ if (isLtr()) {
+ setCursor(selection.max)
+ } else {
+ setCursor(selection.min)
+ }
+ }
+ }
+
+ fun moveCursorPrev() = apply {
+ val prev = annotatedString.text.findPrecedingBreak(selection.end)
+ if (prev != -1) setCursor(prev)
+ }
+
+ fun moveCursorNext() = apply {
+ val next = annotatedString.text.findFollowingBreak(selection.end)
+ if (next != -1) setCursor(next)
+ }
+
+ fun moveCursorToHome() = apply {
+ setCursor(0)
+ }
+
+ fun moveCursorToEnd() = apply {
+ setCursor(text.length)
+ }
+
+ fun moveCursorLeftByWord() = apply {
+ if (isLtr()) {
+ moveCursorPrevByWord()
+ } else {
+ moveCursorNextByWord()
+ }
+ }
+
+ fun moveCursorRightByWord() = apply {
+ if (isLtr()) {
+ moveCursorNextByWord()
+ } else {
+ moveCursorPrevByWord()
+ }
+ }
+
+ fun moveCursorNextByWord() = apply {
+ layoutResult?.getNextWordOffset()?.let { setCursor(it) }
+ }
+
+ fun moveCursorPrevByWord() = apply {
+ layoutResult?.getPrevWordOffset()?.let { setCursor(it) }
+ }
+
+ fun moveCursorPrevByParagraph() = apply {
+ setCursor(getParagraphStart())
+ }
+
+ fun moveCursorNextByParagraph() = apply {
+ setCursor(getParagraphEnd())
+ }
+
+ fun moveCursorUpByLine() = apply {
+ layoutResult?.jumpByLinesOffset(-1)?.let { setCursor(it) }
+ }
+
+ fun moveCursorDownByLine() = apply {
+ layoutResult?.jumpByLinesOffset(1)?.let { setCursor(it) }
+ }
+
+ fun moveCursorToLineStart() = apply {
+ layoutResult?.getLineStartByOffset()?.let { setCursor(it) }
+ }
+
+ fun moveCursorToLineEnd() = apply {
+ layoutResult?.getLineEndByOffset()?.let { setCursor(it) }
+ }
+
+ fun moveCursorToLineLeftSide() = apply {
+ if (isLtr()) {
+ moveCursorToLineStart()
+ } else {
+ moveCursorToLineEnd()
+ }
+ }
+
+ fun moveCursorToLineRightSide() = apply {
+ if (isLtr()) {
+ moveCursorToLineEnd()
+ } else {
+ moveCursorToLineStart()
+ }
+ }
+
+ // it selects a text from the original selection start to a current selection end
+ fun selectMovement() = apply {
+ selection = TextRange(originalSelection.start, selection.end)
+ }
+
+ // delete currently selected text and update [selection] and [annotatedString]
+ fun deleteSelected() = apply {
+ val maxChars = text.length
+ val beforeSelection =
+ annotatedString.subSequence(max(0, selection.min - maxChars), selection.min)
+ val afterSelection =
+ annotatedString.subSequence(selection.max, min(selection.max + maxChars, text.length))
+ annotatedString = beforeSelection + afterSelection
+ setCursor(selection.min)
+ }
+
+ private fun isLtr(): Boolean {
+ val direction = layoutResult?.getBidiRunDirection(selection.end)
+ return direction != ResolvedTextDirection.Rtl
+ }
+
+ private fun TextLayoutResult.getNextWordOffset(
+ currentOffset: Int = transformedEndOffset()
+ ): Int {
+ if (currentOffset >= originalText.length) {
+ return originalText.length
+ }
+ val currentWord = getWordBoundary(charOffset(currentOffset))
+ return if (currentWord.end <= currentOffset) {
+ getNextWordOffset(currentOffset + 1)
+ } else {
+ offsetMapping.transformedToOriginal(currentWord.end)
+ }
+ }
+
+ private fun TextLayoutResult.getPrevWordOffset(
+ currentOffset: Int = transformedEndOffset()
+ ): Int {
+ if (currentOffset < 0) {
+ return 0
+ }
+ val currentWord = getWordBoundary(charOffset(currentOffset))
+ return if (currentWord.start >= currentOffset) {
+ getPrevWordOffset(currentOffset - 1)
+ } else {
+ offsetMapping.transformedToOriginal(currentWord.start)
+ }
+ }
+
+ private fun TextLayoutResult.getLineStartByOffset(
+ currentOffset: Int = transformedMinOffset()
+ ): Int {
+ val currentLine = getLineForOffset(currentOffset)
+ return offsetMapping.transformedToOriginal(getLineStart(currentLine))
+ }
+
+ private fun TextLayoutResult.getLineEndByOffset(
+ currentOffset: Int = transformedMaxOffset()
+ ): Int {
+ val currentLine = getLineForOffset(currentOffset)
+ return offsetMapping.transformedToOriginal(getLineEnd(currentLine, true))
+ }
+
+ private fun TextLayoutResult.jumpByLinesOffset(linesAmount: Int): Int {
+ val currentOffset = transformedEndOffset()
+
+ val newLine = getLineForOffset(currentOffset) + linesAmount
+ when {
+ newLine < 0 -> {
+ return 0
+ }
+ newLine >= lineCount -> {
+ return text.length
+ }
+ }
+
+ val y = getLineBottom(newLine) - 1
+ val x = getCursorRect(currentOffset).left.also {
+ if ((isLtr() && it >= getLineRight(newLine)) ||
+ (!isLtr() && it <= getLineLeft(newLine))
+ ) {
+ return getLineEnd(newLine, true)
+ }
+ }
+
+ val newOffset = getOffsetForPosition(Offset(x, y)).let {
+ offsetMapping.transformedToOriginal(it)
+ }
+
+ return newOffset
+ }
+
+ private fun transformedEndOffset(): Int {
+ return offsetMapping.originalToTransformed(originalSelection.end)
+ }
+
+ private fun transformedMinOffset(): Int {
+ return offsetMapping.originalToTransformed(originalSelection.min)
+ }
+
+ private fun transformedMaxOffset(): Int {
+ return offsetMapping.originalToTransformed(originalSelection.max)
+ }
+
+ private fun charOffset(offset: Int) =
+ offset.coerceAtMost(originalText.length - 1)
+
+ private fun getParagraphStart(): Int {
+ var index = selection.min
+ if (index > 0 && text[index - 1] == '\n') {
+ index--
+ }
+ while (index > 0) {
+ if (text[index - 1] == '\n') {
+ return index
+ }
+ index--
+ }
+ return 0
+ }
+
+ private fun getParagraphEnd(): Int {
+ var index = selection.max
+ if (text[index] == '\n') {
+ index++
+ }
+ while (index < text.length - 1) {
+ if (text[index] == '\n') {
+ return index
+ }
+ index++
+ }
+ return text.length
+ }
+}
+
+internal class TextFieldPreparedSelection(
+ val currentValue: TextFieldValue,
+ offsetMapping: OffsetMapping = OffsetMapping.Identity,
+ val layoutResultProxy: TextLayoutResultProxy?
+) : BaseTextPreparedSelection<TextFieldPreparedSelection>(
+ originalText = currentValue.annotatedString,
+ originalSelection = currentValue.selection,
+ offsetMapping = offsetMapping,
+ layoutResult = layoutResultProxy?.value
+) {
+ val value
+ get() = currentValue.copy(
+ annotatedString = annotatedString,
+ selection = selection
+ )
+
+ fun deleteIfSelectedOr(or: TextFieldPreparedSelection.() -> Unit) = apply {
+ if (selection.collapsed) {
+ or(this)
+ } else {
+ deleteSelected()
+ }
+ }
+
+ fun moveCursorUpByPage() = apply {
+ layoutResultProxy?.jumpByPagesOffset(-1)?.let { setCursor(it) }
+ }
+
+ fun moveCursorDownByPage() = apply {
+ layoutResultProxy?.jumpByPagesOffset(1)?.let { setCursor(it) }
+ }
+
+ /**
+ * Returns a cursor position after jumping back or forth by [pagesAmount] number of pages,
+ * where `page` is the visible amount of space in the text field
+ */
+ private fun TextLayoutResultProxy.jumpByPagesOffset(pagesAmount: Int): Int {
+ val visibleInnerTextFieldRect = innerTextFieldCoordinates?.let { inner ->
+ decorationBoxCoordinates?.localBoundingBoxOf(inner)
+ } ?: Rect.Zero
+ val currentOffset = offsetMapping.originalToTransformed(currentValue.selection.end)
+ val currentPos = value.getCursorRect(currentOffset)
+ val x = currentPos.left
+ val y = currentPos.top + visibleInnerTextFieldRect.size.height * pagesAmount
+ return offsetMapping.transformedToOriginal(
+ value.getOffsetForPosition(Offset(x, y))
+ )
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DesktopCoreTextField.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DesktopCoreTextField.desktop.kt
index d5e4a44..e69de29 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DesktopCoreTextField.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/DesktopCoreTextField.desktop.kt
@@ -1,59 +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.text
-
-import androidx.compose.foundation.text.selection.TextFieldSelectionManager
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.input.key.Key
-import androidx.compose.ui.input.key.plus
-import androidx.compose.ui.input.key.shortcuts
-import androidx.compose.ui.platform.DesktopPlatform
-
-private val modifier by lazy {
- when (DesktopPlatform.Current) {
- DesktopPlatform.MacOS -> Key.MetaLeft
- else -> Key.CtrlLeft
- }
-}
-
-private val copyToClipboardKeySet by lazy { modifier + Key.C }
-
-private val pasteFromClipboardKeySet by lazy { modifier + Key.V }
-
-private val cutToClipboardKeySet by lazy { modifier + Key.X }
-
-private val selectAllKeySet by lazy { modifier + Key.A }
-
-internal actual fun Modifier.textFieldKeyboardModifier(
- manager: TextFieldSelectionManager
-): Modifier = composed {
- shortcuts {
- on(copyToClipboardKeySet) {
- manager.copy(false)
- }
- on(pasteFromClipboardKeySet) {
- manager.paste()
- }
- on(cutToClipboardKeySet) {
- manager.cut()
- }
- on(selectAllKeySet) {
- manager.selectAll()
- }
- }
-}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/KeyMapping.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/KeyMapping.desktop.kt
new file mode 100644
index 0000000..f96b451
--- /dev/null
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/KeyMapping.desktop.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.isAltPressed
+import androidx.compose.ui.input.key.isCtrlPressed
+import androidx.compose.ui.input.key.isMetaPressed
+import androidx.compose.ui.input.key.isShiftPressed
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.platform.DesktopPlatform
+
+internal actual val platformDefaultKeyMapping: KeyMapping =
+ when (DesktopPlatform.Current) {
+ DesktopPlatform.MacOS -> {
+ val common = commonKeyMapping(KeyEvent::isMetaPressed)
+ object : KeyMapping {
+ override fun map(event: KeyEvent): KeyCommand? {
+ return when {
+ event.isShiftPressed && event.isAltPressed ->
+ when (event.key) {
+ Key.DirectionLeft -> KeyCommand.SELECT_LEFT_WORD
+ Key.DirectionRight -> KeyCommand.SELECT_RIGHT_WORD
+ Key.DirectionUp -> KeyCommand.SELECT_PREV_PARAGRAPH
+ Key.DirectionDown -> KeyCommand.SELECT_NEXT_PARAGRAPH
+ else -> null
+ }
+ event.isShiftPressed && event.isMetaPressed ->
+ when (event.key) {
+ Key.DirectionLeft -> KeyCommand.SELECT_LINE_LEFT
+ Key.DirectionRight -> KeyCommand.SELECT_LINE_RIGHT
+ Key.DirectionUp -> KeyCommand.SELECT_HOME
+ Key.DirectionDown -> KeyCommand.SELECT_END
+ else -> null
+ }
+
+ event.isMetaPressed ->
+ when (event.key) {
+ Key.DirectionLeft -> KeyCommand.LINE_LEFT
+ Key.DirectionRight -> KeyCommand.LINE_RIGHT
+ Key.DirectionUp -> KeyCommand.HOME
+ Key.DirectionDown -> KeyCommand.END
+ Key.Backspace -> KeyCommand.DELETE_FROM_LINE_START
+ else -> null
+ }
+
+ // Emacs-like shortcuts
+ event.isCtrlPressed && event.isShiftPressed && event.isAltPressed -> {
+ when (event.key) {
+ Key.F -> KeyCommand.SELECT_RIGHT_WORD
+ Key.B -> KeyCommand.SELECT_LEFT_WORD
+ else -> null
+ }
+ }
+ event.isCtrlPressed && event.isAltPressed -> {
+ when (event.key) {
+ Key.F -> KeyCommand.RIGHT_WORD
+ Key.B -> KeyCommand.LEFT_WORD
+ else -> null
+ }
+ }
+ event.isCtrlPressed && event.isShiftPressed -> {
+ when (event.key) {
+ Key.F -> KeyCommand.SELECT_RIGHT_CHAR
+ Key.B -> KeyCommand.SELECT_LEFT_CHAR
+ Key.P -> KeyCommand.SELECT_UP
+ Key.N -> KeyCommand.SELECT_DOWN
+ Key.A -> KeyCommand.SELECT_LINE_START
+ Key.E -> KeyCommand.SELECT_LINE_END
+ else -> null
+ }
+ }
+ event.isCtrlPressed -> {
+ when (event.key) {
+ Key.F -> KeyCommand.LEFT_CHAR
+ Key.B -> KeyCommand.RIGHT_CHAR
+ Key.P -> KeyCommand.UP
+ Key.N -> KeyCommand.DOWN
+ Key.A -> KeyCommand.LINE_START
+ Key.E -> KeyCommand.LINE_END
+ Key.H -> KeyCommand.DELETE_PREV_CHAR
+ Key.D -> KeyCommand.DELETE_NEXT_CHAR
+ Key.K -> KeyCommand.DELETE_TO_LINE_END
+ Key.O -> KeyCommand.NEW_LINE
+ else -> null
+ }
+ }
+ // end of emacs-like shortcuts
+
+ event.isShiftPressed ->
+ when (event.key) {
+ Key.MoveHome -> KeyCommand.SELECT_HOME
+ Key.MoveEnd -> KeyCommand.SELECT_END
+ else -> null
+ }
+ event.isAltPressed ->
+ when (event.key) {
+ Key.DirectionLeft -> KeyCommand.LEFT_WORD
+ Key.DirectionRight -> KeyCommand.RIGHT_WORD
+ Key.DirectionUp -> KeyCommand.PREV_PARAGRAPH
+ Key.DirectionDown -> KeyCommand.NEXT_PARAGRAPH
+ Key.Delete -> KeyCommand.DELETE_NEXT_WORD
+ Key.Backspace -> KeyCommand.DELETE_PREV_WORD
+ else -> null
+ }
+ else -> null
+ } ?: common.map(event)
+ }
+ }
+ }
+
+ else -> defaultKeyMapping
+ }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.desktop.kt
new file mode 100644
index 0000000..7e88a87
--- /dev/null
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.desktop.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.ui.input.key.KeyEvent
+
+private fun Char.isPrintable(): Boolean {
+ val block = Character.UnicodeBlock.of(this)
+ return (!Character.isISOControl(this)) &&
+ this != java.awt.event.KeyEvent.CHAR_UNDEFINED &&
+ block != null &&
+ block != Character.UnicodeBlock.SPECIALS
+}
+
+actual val KeyEvent.isTypedEvent: Boolean
+ get() = nativeKeyEvent.id == java.awt.event.KeyEvent.KEY_TYPED &&
+ nativeKeyEvent.keyChar.isPrintable()
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.desktop.kt
similarity index 74%
copy from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
copy to compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.desktop.kt
index 7e01354..8ccabc0 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.desktop.kt
@@ -1,5 +1,3 @@
-// ktlint-disable filename
-
/*
* Copyright 2021 The Android Open Source Project
*
@@ -16,6 +14,9 @@
* limitations under the License.
*/
-package androidx.compose.foundation
+package androidx.compose.foundation.text.selection
-internal actual typealias AtomicReference<V> = java.util.concurrent.atomic.AtomicReference<V>
\ No newline at end of file
+import androidx.compose.ui.input.pointer.PointerEvent
+
+internal actual val PointerEvent.isShiftPressed: Boolean
+ get() = mouseEvent?.isShiftDown ?: false
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/text/selection/DesktopTextFieldSelectionManagerTest.kt b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/text/selection/DesktopTextFieldSelectionManagerTest.kt
index b81bdd8..087ae76 100644
--- a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/text/selection/DesktopTextFieldSelectionManagerTest.kt
+++ b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/text/selection/DesktopTextFieldSelectionManagerTest.kt
@@ -18,14 +18,19 @@
import androidx.compose.foundation.text.InternalFoundationTextApi
import androidx.compose.foundation.text.TextFieldState
+import androidx.compose.foundation.text.TextLayoutResultProxy
+import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.InternalTextApi
import androidx.compose.ui.text.TextLayoutInput
+import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.EditProcessor
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow
@@ -33,6 +38,7 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import com.google.common.truth.Truth.assertThat
+import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
@@ -49,35 +55,35 @@
private val offsetMapping = OffsetMapping.Identity
private var value = TextFieldValue(text)
private val lambda: (TextFieldValue) -> Unit = { value = it }
- private val state = TextFieldState(mock())
+ private lateinit var state: TextFieldState
private val dragBeginPosition = Offset.Zero
- private val dragLastPosition = Offset(300f, 15f)
+ private val dragDistance = Offset(300f, 15f)
private val beginOffset = 0
private val dragOffset = text.indexOf('r')
- private val fakeTextRange = TextRange(0, "Hello".length)
- private val dragTextRange = TextRange("Hello".length + 1, text.length)
-
- private val manager = TextFieldSelectionManager()
+ private val layoutResult: TextLayoutResult = mock()
+ private val layoutResultProxy: TextLayoutResultProxy = mock()
+ private lateinit var manager: TextFieldSelectionManager
private val clipboardManager = mock<ClipboardManager>()
private val textToolbar = mock<TextToolbar>()
+ private val hapticFeedback = mock<HapticFeedback>()
+ private val focusRequester = mock<FocusRequester>()
+ private val processor = mock<EditProcessor>()
- @OptIn(InternalFoundationTextApi::class, InternalTextApi::class)
+ @OptIn(InternalFoundationTextApi::class)
@Before
fun setup() {
+ manager = TextFieldSelectionManager()
manager.offsetMapping = offsetMapping
manager.onValueChange = lambda
- manager.state = state
manager.value = value
manager.clipboardManager = clipboardManager
manager.textToolbar = textToolbar
+ manager.hapticFeedBack = hapticFeedback
+ manager.focusRequester = focusRequester
- state.layoutResult = mock()
- state.textDelegate = mock()
- whenever(state.layoutResult!!.value).thenReturn(mock())
- whenever(state.textDelegate.density).thenReturn(density)
- whenever(state.layoutResult!!.value.layoutInput).thenReturn(
+ whenever(layoutResult.layoutInput).thenReturn(
TextLayoutInput(
text = AnnotatedString(text),
style = TextStyle.Default,
@@ -91,38 +97,64 @@
constraints = Constraints()
)
)
- whenever(state.layoutResult!!.getOffsetForPosition(dragBeginPosition)).thenReturn(
- beginOffset
- )
- whenever(state.layoutResult!!.getOffsetForPosition(dragLastPosition)).thenReturn(dragOffset)
- state.processor.reset(value, state.inputSession)
+ whenever(layoutResult.getBoundingBox(any())).thenReturn(Rect.Zero)
+ whenever(layoutResult.getOffsetForPosition(dragBeginPosition)).thenReturn(beginOffset)
+ whenever(layoutResult.getOffsetForPosition(dragBeginPosition + dragDistance))
+ .thenReturn(dragOffset)
+ whenever(
+ layoutResultProxy.getOffsetForPosition(dragBeginPosition, false)
+ ).thenReturn(beginOffset)
+ whenever(
+ layoutResultProxy.getOffsetForPosition(dragBeginPosition + dragDistance, false)
+ ).thenReturn(dragOffset)
+ whenever(
+ layoutResultProxy.getOffsetForPosition(dragBeginPosition + dragDistance)
+ ).thenReturn(dragOffset)
+
+ whenever(layoutResultProxy.value).thenReturn(layoutResult)
+
+ state = TextFieldState(mock())
+ state.layoutResult = layoutResultProxy
+ state.processor.reset(value, null)
+ manager.state = state
+ whenever(state.textDelegate.density).thenReturn(density)
}
@Test
fun TextFieldSelectionManager_mouseSelectionObserver_onStart() {
- manager.mouseSelectionObserver { }.onStart(dragBeginPosition)
+ manager.mouseSelectionObserver.onDown(dragBeginPosition, false)
assertThat(value.selection).isEqualTo(TextRange(0, 0))
- manager.mouseSelectionObserver { }.onStart(dragLastPosition)
+ manager.mouseSelectionObserver.onDown(dragBeginPosition + dragDistance, false)
assertThat(value.selection).isEqualTo(TextRange(8, 8))
}
@Test
+ fun TextFieldSelectionManager_mouseSelectionObserver_onStart_withShift() {
+ manager.mouseSelectionObserver.onDown(dragBeginPosition, false)
+
+ assertThat(value.selection).isEqualTo(TextRange(0, 0))
+
+ manager.mouseSelectionObserver.onDown(dragBeginPosition + dragDistance, true)
+ assertThat(value.selection).isEqualTo(TextRange(0, 8))
+ }
+
+ @Test
fun TextFieldSelectionManager_mouseSelectionObserver_onDrag() {
- val observer = manager.mouseSelectionObserver { }
- observer.onStart(dragBeginPosition)
- observer.onDrag(dragLastPosition)
+ val observer = manager.mouseSelectionObserver
+ observer.onDown(dragBeginPosition, false)
+ observer.onDrag(dragDistance)
assertThat(value.selection).isEqualTo(TextRange(0, 8))
}
@Test
fun TextFieldSelectionManager_mouseSelectionObserver_copy() {
- val observer = manager.mouseSelectionObserver { }
- observer.onStart(dragBeginPosition)
- observer.onDrag(dragLastPosition)
+ val observer = manager.mouseSelectionObserver
+ observer.onDown(dragBeginPosition, false)
+ observer.onDrag(dragDistance)
manager.value = value
manager.copy(cancelSelection = false)
diff --git a/compose/foundation/foundation/src/jvmMain/kotlin/androidx/compose/foundation/text/StringHelpers.jvm.kt b/compose/foundation/foundation/src/jvmMain/kotlin/androidx/compose/foundation/text/StringHelpers.jvm.kt
new file mode 100644
index 0000000..83d6794
--- /dev/null
+++ b/compose/foundation/foundation/src/jvmMain/kotlin/androidx/compose/foundation/text/StringHelpers.jvm.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import java.text.BreakIterator
+
+internal actual fun StringBuilder.appendCodePointX(codePoint: Int): StringBuilder =
+ this.appendCodePoint(codePoint)
+
+internal actual fun String.findPrecedingBreak(index: Int): Int {
+ val it = BreakIterator.getCharacterInstance()
+ it.setText(this)
+ return it.preceding(index)
+}
+
+internal actual fun String.findFollowingBreak(index: Int): Int {
+ val it = BreakIterator.getCharacterInstance()
+ it.setText(this)
+ return it.following(index)
+}
\ No newline at end of file
diff --git a/compose/integration-tests/benchmark/build.gradle b/compose/integration-tests/benchmark/build.gradle
index 6ef5588..dd47137 100644
--- a/compose/integration-tests/benchmark/build.gradle
+++ b/compose/integration-tests/benchmark/build.gradle
@@ -44,7 +44,7 @@
androidTestImplementation(project(":compose:foundation:foundation"))
androidTestImplementation(project(":compose:material:material"))
androidTestImplementation(project(":compose:runtime:runtime"))
- androidTestImplementation(project(":compose:test-utils"))
+ androidTestImplementation(project(":compose:benchmark-utils"))
androidTestImplementation(project(":compose:ui:ui"))
androidTestImplementation(project(":activity:activity-compose"))
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/compose/integration-tests/benchmark/lint-baseline.xml b/compose/integration-tests/benchmark/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/integration-tests/benchmark/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/integration-tests/benchmark/src/androidTest/AndroidManifest.xml b/compose/integration-tests/benchmark/src/androidTest/AndroidManifest.xml
index d50b55c..c828266 100644
--- a/compose/integration-tests/benchmark/src/androidTest/AndroidManifest.xml
+++ b/compose/integration-tests/benchmark/src/androidTest/AndroidManifest.xml
@@ -27,6 +27,5 @@
<!-- enable profileableByShell for non-intrusive profiling tools -->
<!--suppress AndroidElementNotAllowed -->
<profileable android:shell="true"/>
- <activity android:name="androidx.ui.pointerinput.TestActivity" />
</application>
</manifest>
diff --git a/compose/integration-tests/demos/common/lint-baseline.xml b/compose/integration-tests/demos/common/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/integration-tests/demos/common/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/integration-tests/demos/lint-baseline.xml b/compose/integration-tests/demos/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/integration-tests/demos/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/integration-tests/docs-snippets/lint-baseline.xml b/compose/integration-tests/docs-snippets/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/integration-tests/docs-snippets/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/animation/Animation.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/animation/Animation.kt
index 4b2b047..f674d16 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/animation/Animation.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/animation/Animation.kt
@@ -101,7 +101,6 @@
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import org.junit.Rule
@@ -304,7 +303,7 @@
}
@Composable
-fun TargetBasedAnimationSimple(someCustomCondition: () -> Boolean, scope: CoroutineScope) {
+fun TargetBasedAnimationSimple(someCustomCondition: () -> Boolean) {
val anim = remember {
TargetBasedAnimation(
animationSpec = tween(200),
@@ -315,7 +314,7 @@
}
var playTime by remember { mutableStateOf(0L) }
- scope.launch {
+ LaunchedEffect(anim) {
val startTime = withFrameNanos { it }
do {
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/CheatSheet.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/CheatSheet.kt
index 3feae6b..14bfe86 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/CheatSheet.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/CheatSheet.kt
@@ -371,7 +371,6 @@
}
@RequiresApi(Build.VERSION_CODES.O)
-@Composable
private fun TestingCheatSheetOther() {
// COMPOSE TEST RULE
diff --git a/compose/integration-tests/lint-baseline.xml b/compose/integration-tests/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/integration-tests/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/integration-tests/macrobenchmark-target/lint-baseline.xml b/compose/integration-tests/macrobenchmark-target/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/integration-tests/macrobenchmark-target/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/integration-tests/macrobenchmark/lint-baseline.xml b/compose/integration-tests/macrobenchmark/lint-baseline.xml
deleted file mode 100644
index 8f1aa4b..0000000
--- a/compose/integration-tests/macrobenchmark/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/compose/integration-tests/src/main/java/androidx/ui/integration/test/core/ComponentWithModifiersTestCase.kt b/compose/integration-tests/src/main/java/androidx/ui/integration/test/core/ComponentWithModifiersTestCase.kt
deleted file mode 100644
index dbf4fed..0000000
--- a/compose/integration-tests/src/main/java/androidx/ui/integration/test/core/ComponentWithModifiersTestCase.kt
+++ /dev/null
@@ -1,46 +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.ui.integration.test.core
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-
-class ComponentWithModifiersTestCase : SimpleComponentImplenentationTestCase() {
-
- @Composable
- override fun Content() {
- val innerSize = getInnerSize()
- Box(
- Modifier.size(48.dp)
- .background(color = Color.Cyan)
- .padding(innerSize.value)
- .border(
- color = Color.Cyan,
- width = 1.dp,
- shape = CircleShape
- )
- )
- }
-}
\ No newline at end of file
diff --git a/compose/integration-tests/src/main/java/androidx/ui/integration/test/core/ComponentWithRedrawTestCase.kt b/compose/integration-tests/src/main/java/androidx/ui/integration/test/core/ComponentWithRedrawTestCase.kt
deleted file mode 100644
index 149ee4a..0000000
--- a/compose/integration-tests/src/main/java/androidx/ui/integration/test/core/ComponentWithRedrawTestCase.kt
+++ /dev/null
@@ -1,38 +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.ui.integration.test.core
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.foundation.Canvas
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.drawscope.Stroke
-import androidx.compose.foundation.layout.size
-import androidx.compose.ui.unit.dp
-
-class ComponentWithRedrawTestCase : SimpleComponentImplenentationTestCase() {
-
- @Composable
- override fun Content() {
- val innerSize = getInnerSize()
- val stroke = Stroke()
- Canvas(Modifier.size(48.dp)) {
- drawCircle(Color.Black, size.minDimension, style = stroke)
- drawCircle(Color.Black, innerSize.value.value / 2f, center)
- }
- }
-}
diff --git a/compose/integration-tests/src/main/java/androidx/ui/integration/test/core/ComponentWithTwoLayoutNodesTestCase.kt b/compose/integration-tests/src/main/java/androidx/ui/integration/test/core/ComponentWithTwoLayoutNodesTestCase.kt
deleted file mode 100644
index b0482eb..0000000
--- a/compose/integration-tests/src/main/java/androidx/ui/integration/test/core/ComponentWithTwoLayoutNodesTestCase.kt
+++ /dev/null
@@ -1,52 +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.ui.integration.test.core
-
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CircleShape
-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.drawOutline
-import androidx.compose.ui.unit.dp
-
-class ComponentWithTwoLayoutNodesTestCase : SimpleComponentImplenentationTestCase() {
- @Composable
- override fun Content() {
- Box(
- modifier = Modifier
- .size(48.dp)
- .border(BorderStroke(1.dp, Color.Cyan), CircleShape)
- .padding(1.dp),
- contentAlignment = Alignment.Center
- ) {
- val innerSize = getInnerSize().value
- Canvas(Modifier.size(innerSize)) {
- drawOutline(
- CircleShape.createOutline(size, layoutDirection, this),
- Color.Cyan
- )
- }
- }
- }
-}
diff --git a/compose/integration-tests/src/main/java/androidx/ui/integration/test/core/EmptyTestCase.kt b/compose/integration-tests/src/main/java/androidx/ui/integration/test/core/EmptyTestCase.kt
deleted file mode 100644
index 8947a4b..0000000
--- a/compose/integration-tests/src/main/java/androidx/ui/integration/test/core/EmptyTestCase.kt
+++ /dev/null
@@ -1,26 +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.ui.integration.test.core
-
-import androidx.compose.runtime.Composable
-import androidx.compose.testutils.ComposeTestCase
-
-class EmptyTestCase : ComposeTestCase {
- @Composable
- override fun Content() {
- }
-}
\ No newline at end of file
diff --git a/compose/integration-tests/src/main/java/androidx/ui/integration/test/core/SimpleComponentImplenentationTestCase.kt b/compose/integration-tests/src/main/java/androidx/ui/integration/test/core/SimpleComponentImplenentationTestCase.kt
deleted file mode 100644
index c992384..0000000
--- a/compose/integration-tests/src/main/java/androidx/ui/integration/test/core/SimpleComponentImplenentationTestCase.kt
+++ /dev/null
@@ -1,48 +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.ui.integration.test.core
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.testutils.ComposeTestCase
-import androidx.compose.testutils.ToggleableTestCase
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-
-abstract class SimpleComponentImplenentationTestCase : ComposeTestCase, ToggleableTestCase {
-
- private var state: MutableState<Dp>? = null
-
- @Composable
- fun getInnerSize(): MutableState<Dp> {
- val innerSize = remember { mutableStateOf(10.dp) }
- state = innerSize
- return innerSize
- }
-
- override fun toggleState() {
- with(state!!) {
- value = if (value == 10.dp) {
- 20.dp
- } else {
- 10.dp
- }
- }
- }
-}
diff --git a/compose/internal-lint-checks/lint-baseline.xml b/compose/internal-lint-checks/lint-baseline.xml
deleted file mode 100644
index 297ae16..0000000
--- a/compose/internal-lint-checks/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" version="4.2.0-beta02">
-
-</issues>
diff --git a/compose/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt b/compose/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt
index 3806120..2cde8e4 100644
--- a/compose/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt
+++ b/compose/internal-lint-checks/src/main/java/androidx/compose/lint/UnnecessaryLambdaCreationDetector.kt
@@ -145,8 +145,9 @@
val expectedComposable = when (val source = parentDeclaration.sourcePsi) {
// The source is in Kotlin source, so check the parameter for @Composable
is KtCallableDeclaration -> {
- // Currently type annotations don't appear on the psiType, so we have to look
- // through the type reference (https://youtrack.jetbrains.com/issue/KT-45244)
+ // Currently type annotations don't appear on the psiType in the version of
+ // UAST / PSI we are using, so we have to look through the type reference.
+ // Should be fixed when Lint upgrades the version to 1.4.30+.
val typeReference = source.valueParameters[parameterIndex]!!
.typeReference as KtTypeReference
typeReference.annotationEntries.any {
@@ -156,7 +157,7 @@
// If we cannot resolve the parent expression as a KtCallableDeclaration, then
// the source is Java source, or in a class file. We ignore Java source, and
// currently there is no way to see the @Composable annotation in the class file
- // (https://youtrack.jetbrains.com/issue/KT-45244). Instead we can look for the
+ // (https://youtrack.jetbrains.com/issue/KT-45307). Instead we can look for the
// presence of a `Composer` parameter, as this is added by the Compose compiler
// to every Composable function / lambda.
else -> {
diff --git a/compose/material/material-icons-core/lint-baseline.xml b/compose/material/material-icons-core/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/material/material-icons-core/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/material/material-icons-core/samples/lint-baseline.xml b/compose/material/material-icons-core/samples/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/material/material-icons-core/samples/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/material/material-ripple/lint-baseline.xml b/compose/material/material-ripple/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/material/material-ripple/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/material/material/api/public_plus_experimental_1.0.0-beta02.txt b/compose/material/material/api/public_plus_experimental_1.0.0-beta02.txt
index 72bd10a..2795abb 100644
--- a/compose/material/material/api/public_plus_experimental_1.0.0-beta02.txt
+++ b/compose/material/material/api/public_plus_experimental_1.0.0-beta02.txt
@@ -664,6 +664,7 @@
}
public final class TabKt {
+ method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void LeadingIconTab-PWX9des(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> text, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional long selectedContentColor, optional long unselectedContentColor);
method @androidx.compose.runtime.Composable public static void Tab-TC9MJzw(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional long selectedContentColor, optional long unselectedContentColor);
method @androidx.compose.runtime.Composable public static void Tab-wUuQ7UU(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional long selectedContentColor, optional long unselectedContentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
}
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index 72bd10a..2795abb 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -664,6 +664,7 @@
}
public final class TabKt {
+ method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void LeadingIconTab-PWX9des(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> text, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional long selectedContentColor, optional long unselectedContentColor);
method @androidx.compose.runtime.Composable public static void Tab-TC9MJzw(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional long selectedContentColor, optional long unselectedContentColor);
method @androidx.compose.runtime.Composable public static void Tab-wUuQ7UU(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional long selectedContentColor, optional long unselectedContentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
}
diff --git a/compose/material/material/benchmark/build.gradle b/compose/material/material/benchmark/build.gradle
index 91478ed..1c2ce1b 100644
--- a/compose/material/material/benchmark/build.gradle
+++ b/compose/material/material/benchmark/build.gradle
@@ -35,7 +35,7 @@
androidTestImplementation project(":compose:foundation:foundation")
androidTestImplementation project(":compose:material:material")
androidTestImplementation project(":compose:runtime:runtime")
- androidTestImplementation project(":compose:test-utils")
+ androidTestImplementation project(":compose:benchmark-utils")
androidTestImplementation(ANDROIDX_TEST_RULES)
androidTestImplementation(JUNIT)
androidTestImplementation(KOTLIN_STDLIB)
diff --git a/compose/material/material/icons/generator/lint-baseline.xml b/compose/material/material/icons/generator/lint-baseline.xml
deleted file mode 100644
index 297ae16..0000000
--- a/compose/material/material/icons/generator/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" version="4.2.0-beta02">
-
-</issues>
diff --git a/compose/material/material/integration-tests/material-demos/lint-baseline.xml b/compose/material/material/integration-tests/material-demos/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/material/material/integration-tests/material-demos/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialDemos.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialDemos.kt
index d3c01a0..aeb2681 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialDemos.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialDemos.kt
@@ -45,7 +45,13 @@
ComposableDemo("App Bars") { AppBarDemo() },
ComposableDemo("Backdrop") { BackdropScaffoldSample() },
ComposableDemo("Bottom Navigation") { BottomNavigationDemo() },
- ComposableDemo("Bottom Sheet") { BottomSheetScaffoldSample() },
+ DemoCategory(
+ "Bottom Sheets",
+ listOf(
+ ComposableDemo("Bottom Sheet") { BottomSheetScaffoldSample() },
+ ComposableDemo("Modal Bottom Sheet") { ModalBottomSheetSample() },
+ )
+ ),
ComposableDemo("Buttons & FABs") { ButtonDemo() },
DemoCategory(
"Navigation drawer",
@@ -72,7 +78,6 @@
ActivityDemo("Dynamic Theme", DynamicThemeActivity::class)
)
),
- ComposableDemo("Modal bottom sheet") { ModalBottomSheetSample() },
ComposableDemo("Progress Indicators") { ProgressIndicatorDemo() },
DemoCategory(
"Scaffold",
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 1981844..43a42d1 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
@@ -32,6 +32,7 @@
import androidx.compose.material.samples.ScrollingFancyIndicatorContainerTabs
import androidx.compose.material.samples.ScrollingTextTabs
import androidx.compose.material.samples.TextAndIconTabs
+import androidx.compose.material.samples.LeadingIconTabs
import androidx.compose.material.samples.TextTabs
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
@@ -55,6 +56,8 @@
Spacer(Modifier.requiredHeight(24.dp))
TextAndIconTabs()
Spacer(Modifier.requiredHeight(24.dp))
+ LeadingIconTabs()
+ Spacer(Modifier.requiredHeight(24.dp))
ScrollingTextTabs()
} else {
FancyTabs()
diff --git a/compose/material/material/integration-tests/material-studies/lint-baseline.xml b/compose/material/material/integration-tests/material-studies/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/material/material/integration-tests/material-studies/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/material/material/lint-baseline.xml b/compose/material/material/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/material/material/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/material/material/samples/lint-baseline.xml b/compose/material/material/samples/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/material/material/samples/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/DrawerSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/DrawerSamples.kt
index 7175e72..5e3e453 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/DrawerSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/DrawerSamples.kt
@@ -18,20 +18,31 @@
import androidx.annotation.Sampled
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.padding
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.selection.toggleable
import androidx.compose.material.BottomDrawer
import androidx.compose.material.BottomDrawerValue
import androidx.compose.material.Button
+import androidx.compose.material.Checkbox
import androidx.compose.material.DrawerValue
import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Icon
+import androidx.compose.material.ListItem
import androidx.compose.material.ModalDrawer
import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.rememberBottomDrawerState
import androidx.compose.material.rememberDrawerState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -71,28 +82,55 @@
@Composable
@OptIn(ExperimentalMaterialApi::class)
fun BottomDrawerSample() {
- val drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+ val (gesturesEnabled, toggleGesturesEnabled) = remember { mutableStateOf(true) }
val scope = rememberCoroutineScope()
- BottomDrawer(
- drawerState = drawerState,
- drawerContent = {
- Button(
- modifier = Modifier.align(Alignment.CenterHorizontally).padding(top = 16.dp),
- onClick = { scope.launch { drawerState.close() } },
- content = { Text("Close Drawer") }
+ Column {
+ Row(
+ modifier = Modifier.fillMaxWidth().toggleable(
+ value = gesturesEnabled,
+ onValueChange = toggleGesturesEnabled
)
- },
- content = {
- Column(
- modifier = Modifier.fillMaxSize().padding(16.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(text = if (drawerState.isClosed) "▲▲▲ Pull up ▲▲▲" else "▼▼▼ Drag down ▼▼▼")
- Spacer(Modifier.height(20.dp))
- Button(onClick = { scope.launch { drawerState.open() } }) {
- Text("Click to open")
+ ) {
+ Checkbox(gesturesEnabled, null)
+ Text(text = if (gesturesEnabled) "Gestures Enabled" else "Gestures Disabled")
+ }
+ val drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+ BottomDrawer(
+ gesturesEnabled = gesturesEnabled,
+ drawerState = drawerState,
+ drawerContent = {
+ Button(
+ modifier = Modifier.align(Alignment.CenterHorizontally).padding(top = 16.dp),
+ onClick = { scope.launch { drawerState.close() } },
+ content = { Text("Close Drawer") }
+ )
+ LazyColumn {
+ items(5) {
+ ListItem(
+ text = { Text("Item $it") },
+ icon = {
+ Icon(
+ Icons.Default.Favorite,
+ contentDescription = "Localized description"
+ )
+ }
+ )
+ }
+ }
+ },
+ content = {
+ Column(
+ modifier = Modifier.fillMaxSize().padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ val openText = if (gesturesEnabled) "▲▲▲ Pull up ▲▲▲" else "Click the button!"
+ Text(text = if (drawerState.isClosed) openText else "▼▼▼ Drag down ▼▼▼")
+ Spacer(Modifier.height(20.dp))
+ Button(onClick = { scope.launch { drawerState.open() } }) {
+ Text("Click to open")
+ }
}
}
- }
- )
+ )
+ }
}
\ No newline at end of file
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 e0d0fdd..3d76467 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
@@ -36,6 +36,7 @@
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ScrollableTabRow
@@ -43,6 +44,7 @@
import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material.TabPosition
import androidx.compose.material.TabRow
+import androidx.compose.material.LeadingIconTab
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
@@ -128,6 +130,34 @@
}
}
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun LeadingIconTabs() {
+ var state by remember { mutableStateOf(0) }
+ val titlesAndIcons = listOf(
+ "TAB" to Icons.Filled.Favorite,
+ "TAB & ICON" to Icons.Filled.Favorite,
+ "TAB 3 WITH LOTS OF TEXT" to Icons.Filled.Favorite
+ )
+ Column {
+ TabRow(selectedTabIndex = state) {
+ titlesAndIcons.forEachIndexed { index, (title, icon) ->
+ LeadingIconTab(
+ text = { Text(title) },
+ icon = { Icon(icon, contentDescription = null) },
+ selected = state == index,
+ onClick = { state = index }
+ )
+ }
+ }
+ Text(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ text = "Leading icon tab ${state + 1} selected",
+ style = MaterialTheme.typography.body1
+ )
+ }
+}
+
@Composable
fun ScrollingTextTabs() {
var state by remember { mutableStateOf(0) }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
index 74681ac..21bfc21 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
@@ -21,18 +21,23 @@
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.GestureScope
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.bottomCenter
+import androidx.compose.ui.test.center
import androidx.compose.ui.test.centerLeft
import androidx.compose.ui.test.click
import androidx.compose.ui.test.junit4.createComposeRule
@@ -41,20 +46,22 @@
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performGesture
import androidx.compose.ui.test.performSemanticsAction
-import androidx.compose.ui.test.swipeDown
+import androidx.compose.ui.test.swipe
import androidx.compose.ui.test.swipeLeft
import androidx.compose.ui.test.swipeRight
-import androidx.compose.ui.test.swipeUp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
+import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import kotlin.math.roundToInt
@MediumTest
@RunWith(AndroidJUnit4::class)
@@ -64,6 +71,13 @@
@get:Rule
val rule = createComposeRule()
+ private val bottomDrawerTag = "drawerContentTag"
+ private val shortBottomDrawerHeight = 256.dp
+
+ private fun advanceClock() {
+ rule.mainClock.advanceTimeBy(100_000L)
+ }
+
@Test
fun modalDrawer_testOffset_whenOpen() {
rule.setMaterialContent {
@@ -117,13 +131,70 @@
}
@Test
- fun bottomDrawer_testOffset_whenOpen() {
+ fun bottomDrawer_testOffset_shortDrawer_whenClosed() {
+ rule.setMaterialContent {
+ val drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+ BottomDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ Box(Modifier.fillMaxSize().testTag(bottomDrawerTag))
+ },
+ content = {}
+ )
+ }
+
+ val height = rule.rootHeight()
+ rule.onNodeWithTag(bottomDrawerTag)
+ .assertTopPositionInRootIsEqualTo(height)
+ }
+
+ @Test
+ fun bottomDrawer_testOffset_shortDrawer_whenExpanded() {
+ rule.setMaterialContent {
+ val drawerState = rememberBottomDrawerState(BottomDrawerValue.Expanded)
+ BottomDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ Box(Modifier.height(shortBottomDrawerHeight).testTag(bottomDrawerTag))
+ },
+ content = {}
+ )
+ }
+
+ val height = rule.rootHeight()
+ val expectedTop = height - shortBottomDrawerHeight
+ rule.onNodeWithTag(bottomDrawerTag)
+ .assertTopPositionInRootIsEqualTo(expectedTop)
+ }
+
+ @Test
+ fun bottomDrawer_testOffset_tallDrawer_whenClosed() {
+ rule.setMaterialContent {
+ val drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+ BottomDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ Box(Modifier.fillMaxSize().testTag(bottomDrawerTag))
+ },
+ content = {}
+ )
+ }
+
+ val height = rule.rootHeight()
+ val expectedTop = height
+ rule.onNodeWithTag(bottomDrawerTag)
+ .assertTopPositionInRootIsEqualTo(expectedTop)
+ }
+
+ @Test
+ @Ignore // Disabled until b/178529942 is fixed
+ fun bottomDrawer_testOffset_tallDrawer_whenOpen() {
rule.setMaterialContent {
val drawerState = rememberBottomDrawerState(BottomDrawerValue.Open)
BottomDrawer(
drawerState = drawerState,
drawerContent = {
- Box(Modifier.fillMaxSize().testTag("content"))
+ Box(Modifier.fillMaxSize().testTag(bottomDrawerTag))
},
content = {}
)
@@ -132,26 +203,44 @@
val width = rule.rootWidth()
val height = rule.rootHeight()
val expectedTop = if (width > height) 0.dp else (height / 2)
- rule.onNodeWithTag("content")
+ rule.onNodeWithTag(bottomDrawerTag)
.assertTopPositionInRootIsEqualTo(expectedTop)
}
@Test
- fun bottomDrawer_testOffset_whenClosed() {
+ fun bottomDrawer_testOffset_tallDrawer_whenExpanded() {
rule.setMaterialContent {
- val drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+ val drawerState = rememberBottomDrawerState(BottomDrawerValue.Expanded)
BottomDrawer(
drawerState = drawerState,
drawerContent = {
- Box(Modifier.fillMaxSize().testTag("content"))
+ Box(Modifier.fillMaxSize().testTag(bottomDrawerTag))
},
content = {}
)
}
- val height = rule.rootHeight()
- rule.onNodeWithTag("content")
- .assertTopPositionInRootIsEqualTo(height)
+ val expectedTop = 0.dp
+ rule.onNodeWithTag(bottomDrawerTag)
+ .assertTopPositionInRootIsEqualTo(expectedTop)
+ }
+
+ @Test
+ @SmallTest
+ fun bottomDrawer_hasPaneTitle() {
+ rule.setMaterialContent {
+ BottomDrawer(
+ drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed),
+ drawerContent = {
+ Box(Modifier.fillMaxSize().testTag(bottomDrawerTag))
+ },
+ content = {}
+ )
+ }
+
+ rule.onNodeWithTag(bottomDrawerTag, useUnmergedTree = true)
+ .onParent()
+ .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.PaneTitle))
}
@Test
@@ -263,125 +352,6 @@
@Test
@LargeTest
- fun bottomDrawer_drawerContent_doesntPropagateClicksWhenOpen(): Unit = runBlocking {
- var bodyClicks = 0
- lateinit var drawerState: BottomDrawerState
- rule.setMaterialContent {
- drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
- BottomDrawer(
- drawerState = drawerState,
- drawerContent = {
- Box(Modifier.fillMaxSize().testTag("Drawer"))
- },
- content = {
- Box(Modifier.fillMaxSize().clickable { bodyClicks += 1 })
- }
- )
- }
-
- // Click in the middle of the drawer
- rule.onNodeWithTag("Drawer").performClick()
-
- rule.runOnIdle {
- assertThat(bodyClicks).isEqualTo(1)
- }
- drawerState.open()
-
- // Click on the left-center pixel of the drawer
- rule.onNodeWithTag("Drawer").performGesture {
- click(centerLeft)
- }
-
- rule.runOnIdle {
- assertThat(bodyClicks).isEqualTo(1)
- }
- drawerState.expand()
-
- // Click on the left-center pixel of the drawer once again in a new state
- rule.onNodeWithTag("Drawer").performGesture {
- click(centerLeft)
- }
-
- rule.runOnIdle {
- assertThat(bodyClicks).isEqualTo(1)
- }
- }
-
- @Test
- @LargeTest
- fun bottomDrawer_openAndClose(): Unit = runBlocking {
- lateinit var drawerState: BottomDrawerState
- rule.setMaterialContent {
- drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
- BottomDrawer(
- drawerState = drawerState,
- drawerContent = {
- Box(Modifier.fillMaxSize().testTag("drawer"))
- },
- content = {}
- )
- }
-
- val width = rule.rootWidth()
- val height = rule.rootHeight()
- val topWhenOpened = if (width > height) 0.dp else (height / 2)
- val topWhenClosed = height
-
- // Drawer should start in closed state
- rule.onNodeWithTag("drawer").assertTopPositionInRootIsEqualTo(topWhenClosed)
-
- // When the drawer state is set to Opened
- drawerState.open()
- // Then the drawer should be opened
- rule.onNodeWithTag("drawer").assertTopPositionInRootIsEqualTo(topWhenOpened)
-
- // When the drawer state is set to Closed
- drawerState.close()
- // Then the drawer should be closed
- rule.onNodeWithTag("drawer").assertTopPositionInRootIsEqualTo(topWhenClosed)
- }
-
- @Test
- fun bottomDrawer_bodyContent_clickable(): Unit = runBlocking {
- var drawerClicks = 0
- var bodyClicks = 0
- lateinit var drawerState: BottomDrawerState
- rule.setMaterialContent {
- drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
- // emulate click on the screen
- BottomDrawer(
- drawerState = drawerState,
- drawerContent = {
- Box(Modifier.fillMaxSize().clickable { drawerClicks += 1 })
- },
- content = {
- Box(Modifier.testTag("Drawer").fillMaxSize().clickable { bodyClicks += 1 })
- }
- )
- }
-
- // Click in the middle of the drawer (which is the middle of the body)
- rule.onNodeWithTag("Drawer").performGesture { click() }
-
- rule.runOnIdle {
- assertThat(drawerClicks).isEqualTo(0)
- assertThat(bodyClicks).isEqualTo(1)
- }
-
- drawerState.open()
- sleep(100) // TODO(147586311): remove this sleep when opening the drawer triggers a wait
-
- // Click on the bottom-center pixel of the drawer
- rule.onNodeWithTag("Drawer").performGesture {
- click(bottomCenter)
- }
-
- assertThat(drawerClicks).isEqualTo(1)
- assertThat(bodyClicks).isEqualTo(1)
- }
-
- @Test
- @LargeTest
fun modalDrawer_openBySwipe() {
lateinit var drawerState: DrawerState
rule.setMaterialContent {
@@ -453,43 +423,6 @@
@Test
@LargeTest
- fun bottomDrawer_openBySwipe() {
- lateinit var drawerState: BottomDrawerState
- rule.setMaterialContent {
- drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
- // emulate click on the screen
- Box(Modifier.testTag("Drawer")) {
- BottomDrawer(
- drawerState = drawerState,
- drawerContent = {
- Box(Modifier.fillMaxSize().background(color = Color.Magenta))
- },
- content = {
- Box(Modifier.fillMaxSize().background(color = Color.Red))
- }
- )
- }
- }
- val isLandscape = rule.rootWidth() > rule.rootHeight()
-
- rule.onNodeWithTag("Drawer")
- .performGesture { swipeUp() }
-
- rule.runOnIdle {
- assertThat(drawerState.currentValue).isEqualTo(
- if (isLandscape) BottomDrawerValue.Open else BottomDrawerValue.Expanded
- )
- }
-
- rule.onNodeWithTag("Drawer")
- .performGesture { swipeDown() }
- rule.runOnIdle {
- assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Closed)
- }
- }
-
- @Test
- @LargeTest
fun modalDrawer_noDismissActionWhenClosed_hasDissmissActionWhenOpen(): Unit = runBlocking {
lateinit var drawerState: DrawerState
rule.setMaterialContent {
@@ -526,6 +459,255 @@
}
@Test
+ fun bottomDrawer_bodyContent_clickable(): Unit = runBlocking {
+ var drawerClicks = 0
+ var bodyClicks = 0
+ lateinit var drawerState: BottomDrawerState
+ rule.setMaterialContent {
+ drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+ // emulate click on the screen
+ BottomDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ Box(Modifier.fillMaxSize().clickable { drawerClicks += 1 })
+ },
+ content = {
+ Box(
+ Modifier
+ .testTag(bottomDrawerTag)
+ .fillMaxSize()
+ .clickable { bodyClicks += 1 }
+ )
+ }
+ )
+ }
+
+ // Click in the middle of the drawer (which is the middle of the body)
+ rule.onNodeWithTag(bottomDrawerTag).performGesture { click() }
+
+ rule.runOnIdle {
+ assertThat(drawerClicks).isEqualTo(0)
+ assertThat(bodyClicks).isEqualTo(1)
+ }
+
+ drawerState.open()
+ sleep(100) // TODO(147586311): remove this sleep when opening the drawer triggers a wait
+
+ // Click on the bottom-center pixel of the drawer
+ rule.onNodeWithTag(bottomDrawerTag).performGesture {
+ click(bottomCenter)
+ }
+
+ assertThat(drawerClicks).isEqualTo(1)
+ assertThat(bodyClicks).isEqualTo(1)
+ }
+
+ @Test
+ @LargeTest
+ fun bottomDrawer_drawerContent_doesntPropagateClicksWhenOpen(): Unit = runBlocking {
+ var bodyClicks = 0
+ lateinit var drawerState: BottomDrawerState
+ rule.setMaterialContent {
+ drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+ BottomDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ Box(Modifier.fillMaxSize().testTag(bottomDrawerTag))
+ },
+ content = {
+ Box(Modifier.fillMaxSize().clickable { bodyClicks += 1 })
+ }
+ )
+ }
+
+ // Click in the middle of the drawer
+ rule.onNodeWithTag(bottomDrawerTag).performClick()
+
+ rule.runOnIdle {
+ assertThat(bodyClicks).isEqualTo(1)
+ }
+ drawerState.open()
+
+ // Click on the left-center pixel of the drawer
+ rule.onNodeWithTag(bottomDrawerTag).performGesture {
+ click(centerLeft)
+ }
+
+ rule.runOnIdle {
+ assertThat(bodyClicks).isEqualTo(1)
+ }
+ drawerState.expand()
+
+ // Click on the left-center pixel of the drawer once again in a new state
+ rule.onNodeWithTag(bottomDrawerTag).performGesture {
+ click(centerLeft)
+ }
+
+ rule.runOnIdle {
+ assertThat(bodyClicks).isEqualTo(1)
+ }
+ }
+
+ @Test
+ @LargeTest
+ fun bottomDrawer_openBySwipe_shortDrawer(): Unit = runBlocking {
+ val contentTag = "contentTestTag"
+ lateinit var drawerState: BottomDrawerState
+ rule.setMaterialContent {
+ drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+ BottomDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ Box(
+ Modifier.height(shortBottomDrawerHeight).testTag(bottomDrawerTag)
+ )
+ },
+ content = { Box(Modifier.fillMaxSize().testTag(contentTag)) }
+ )
+ }
+
+ rule.runOnIdle {
+ assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Closed)
+ }
+
+ rule.onNodeWithTag(contentTag)
+ .performGesture { swipeUp() }
+
+ advanceClock()
+
+ rule.runOnIdle {
+ assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Expanded)
+ }
+
+ rule.onNodeWithTag(bottomDrawerTag)
+ .performGesture { swipeDown() }
+
+ advanceClock()
+
+ rule.runOnIdle {
+ assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Closed)
+ }
+ }
+
+ @Test
+ @LargeTest
+ fun bottomDrawer_expandBySwipe_tallDrawer(): Unit = runBlocking {
+ val contentTag = "contentTestTag"
+ lateinit var drawerState: BottomDrawerState
+ rule.setMaterialContent {
+ drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+ BottomDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ Box(
+ Modifier.fillMaxSize().testTag(bottomDrawerTag)
+ )
+ },
+ content = { Box(Modifier.fillMaxSize().testTag(contentTag)) }
+ )
+ }
+
+ val isLandscape = rule.rootWidth() > rule.rootHeight()
+ val peekHeight = with(rule.density) { rule.rootHeight().toPx() / 2 }
+
+ rule.runOnIdle {
+ assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Closed)
+ }
+
+ rule.onNodeWithTag(contentTag)
+ .performGesture { swipeUp(endY = peekHeight) }
+
+ advanceClock()
+
+ rule.runOnIdle {
+ assertThat(drawerState.currentValue).isEqualTo(
+ if (isLandscape) BottomDrawerValue.Expanded else BottomDrawerValue.Open
+ )
+ }
+
+ rule.onNodeWithTag(bottomDrawerTag)
+ .performGesture { swipeUp() }
+
+ advanceClock()
+
+ rule.runOnIdle {
+ assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Expanded)
+ }
+
+ rule.onNodeWithTag(bottomDrawerTag)
+ .performGesture { swipeDown(endY = peekHeight) }
+
+ advanceClock()
+
+ rule.runOnIdle {
+ assertThat(drawerState.currentValue).isEqualTo(
+ if (isLandscape) BottomDrawerValue.Closed else BottomDrawerValue.Open
+ )
+ }
+
+ rule.onNodeWithTag(bottomDrawerTag)
+ .performGesture { swipeDown() }
+
+ advanceClock()
+
+ rule.runOnIdle {
+ assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Closed)
+ }
+ }
+
+ @Test
+ fun bottomDrawer_openBySwipe_onBodyContent(): Unit = runBlocking {
+ val contentTag = "contentTestTag"
+ lateinit var drawerState: BottomDrawerState
+ rule.setMaterialContent {
+ drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+ BottomDrawer(
+ drawerState = drawerState,
+ drawerContent = { Box(Modifier.height(shortBottomDrawerHeight)) },
+ content = { Box(Modifier.fillMaxSize().testTag(contentTag)) }
+ )
+ }
+
+ rule.runOnIdle {
+ assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Closed)
+ }
+
+ rule.onNodeWithTag(contentTag)
+ .performGesture { swipeUp() }
+
+ advanceClock()
+
+ rule.runOnIdle {
+ assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Expanded)
+ }
+ }
+
+ @Test
+ fun bottomDrawer_hasDismissAction_whenExpanded(): Unit = runBlocking {
+ lateinit var drawerState: BottomDrawerState
+ rule.setMaterialContent {
+ drawerState = rememberBottomDrawerState(BottomDrawerValue.Expanded)
+ BottomDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ Box(Modifier.fillMaxSize().testTag(bottomDrawerTag))
+ },
+ content = {}
+ )
+ }
+
+ val height = rule.rootHeight()
+ rule.onNodeWithTag(bottomDrawerTag).onParent()
+ .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
+ .performSemanticsAction(SemanticsActions.Dismiss)
+
+ advanceClock()
+
+ rule.onNodeWithTag(bottomDrawerTag)
+ .assertTopPositionInRootIsEqualTo(height)
+ }
+
+ @Test
@LargeTest
fun bottomDrawer_noDismissActionWhenClosed_hasDissmissActionWhenOpen(): Unit = runBlocking {
lateinit var drawerState: BottomDrawerState
@@ -534,31 +716,164 @@
BottomDrawer(
drawerState = drawerState,
drawerContent = {
- Box(Modifier.fillMaxSize().testTag("drawer"))
+ Box(Modifier.fillMaxSize().testTag(bottomDrawerTag))
},
content = {}
)
}
// Drawer should start in closed state and have no dismiss action
- rule.onNodeWithTag("drawer", useUnmergedTree = true)
+ assertThat(drawerState.currentValue).isEqualTo(BottomDrawerValue.Closed)
+ rule.onNodeWithTag(bottomDrawerTag, useUnmergedTree = true)
.onParent()
.assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Dismiss))
- // When the drawer state is set to Opened
+ // When the drawer state is set to Open or Expanded
drawerState.open()
+ assertThat(drawerState.currentValue)
+ .isAnyOf(BottomDrawerValue.Open, BottomDrawerValue.Expanded)
// Then the drawer should be opened and have dismiss action
- rule.onNodeWithTag("drawer", useUnmergedTree = true)
+ rule.onNodeWithTag(bottomDrawerTag, useUnmergedTree = true)
.onParent()
.assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
// When the drawer state is set to Closed using dismiss action
- rule.onNodeWithTag("drawer", useUnmergedTree = true)
+ rule.onNodeWithTag(bottomDrawerTag, useUnmergedTree = true)
.onParent()
.performSemanticsAction(SemanticsActions.Dismiss)
// Then the drawer should be closed and have no dismiss action
- rule.onNodeWithTag("drawer", useUnmergedTree = true)
+ rule.onNodeWithTag(bottomDrawerTag, useUnmergedTree = true)
.onParent()
.assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Dismiss))
}
+
+ @Test
+ @LargeTest
+ fun bottomDrawer_openAndClose_shortDrawer(): Unit = runBlocking {
+ lateinit var drawerState: BottomDrawerState
+ rule.setMaterialContent {
+ drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+ BottomDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ Box(Modifier.height(shortBottomDrawerHeight).testTag(bottomDrawerTag))
+ },
+ content = {}
+ )
+ }
+
+ val height = rule.rootHeight()
+ val topWhenOpened = height - shortBottomDrawerHeight
+ val topWhenExpanded = topWhenOpened
+ val topWhenClosed = height
+
+ // Drawer should start in closed state
+ rule.onNodeWithTag(bottomDrawerTag).assertTopPositionInRootIsEqualTo(topWhenClosed)
+
+ // When the drawer state is set to Opened
+ drawerState.open()
+ // Then the drawer should be opened
+ rule.onNodeWithTag(bottomDrawerTag).assertTopPositionInRootIsEqualTo(topWhenOpened)
+
+ // When the drawer state is set to Expanded
+ drawerState.expand()
+ // Then the drawer should be expanded
+ rule.onNodeWithTag(bottomDrawerTag).assertTopPositionInRootIsEqualTo(topWhenExpanded)
+
+ // When the drawer state is set to Closed
+ drawerState.close()
+ // Then the drawer should be closed
+ rule.onNodeWithTag(bottomDrawerTag).assertTopPositionInRootIsEqualTo(topWhenClosed)
+ }
+
+ @Test
+ @LargeTest
+ fun bottomDrawer_openAndClose_tallDrawer(): Unit = runBlocking {
+ lateinit var drawerState: BottomDrawerState
+ rule.setMaterialContent {
+ drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+ BottomDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ Box(Modifier.fillMaxSize().testTag(bottomDrawerTag))
+ },
+ content = {}
+ )
+ }
+
+ val width = rule.rootWidth()
+ val height = rule.rootHeight()
+ val topWhenOpened = if (width > height) 0.dp else (height / 2)
+ val topWhenExpanded = 0.dp
+ val topWhenClosed = height
+
+ // Drawer should start in closed state
+ rule.onNodeWithTag(bottomDrawerTag).assertTopPositionInRootIsEqualTo(topWhenClosed)
+
+ // When the drawer state is set to Opened
+ drawerState.open()
+ // Then the drawer should be opened
+ rule.onNodeWithTag(bottomDrawerTag).assertTopPositionInRootIsEqualTo(topWhenOpened)
+
+ // When the drawer state is set to Expanded
+ drawerState.expand()
+ // Then the drawer should be expanded
+ rule.onNodeWithTag(bottomDrawerTag).assertTopPositionInRootIsEqualTo(topWhenExpanded)
+
+ // When the drawer state is set to Closed
+ drawerState.close()
+ // Then the drawer should be closed
+ rule.onNodeWithTag(bottomDrawerTag).assertTopPositionInRootIsEqualTo(topWhenClosed)
+ }
+
+ /**
+ * Performs a swipe up gesture on the associated node. The gesture starts at [startY] and
+ * ends at [endY].
+ *
+ * @param startY The start position of the gesture. By default, this is slightly above the
+ * bottom of the associated node.
+ * @param endY The end position of the gesture. By default, this is the top of the associated
+ * node.
+ */
+ private fun GestureScope.swipeUp(
+ startY: Float = (visibleSize.height * (1 - edgeFuzzFactor)).roundToInt().toFloat(),
+ endY: Float = 0.0f
+ ) {
+ require(startY >= endY) {
+ "Start position $startY needs to be equal or bigger than end position $endY"
+ }
+ val x = center.x
+ val start = Offset(x, startY)
+ val end = Offset(x, endY)
+ swipe(start, end, 200)
+ }
+
+ /**
+ * Performs a swipe down gesture on the associated node. The gesture starts at [startY] and
+ * ends at [endY].
+ *
+ * @param startY The start position of the gesture. By default, this is slightly below the
+ * top of the associated node.
+ * @param endY The end position of the gesture. By default, this is the bottom of the associated
+ * node.
+ */
+ private fun GestureScope.swipeDown(
+ startY: Float = (visibleSize.height * edgeFuzzFactor).roundToInt().toFloat(),
+ endY: Float = visibleSize.height.toFloat()
+ ) {
+ require(endY >= startY) {
+ "End position $endY needs to be equal or bigger than start position $startY"
+ }
+ val x = center.x
+ val start = Offset(x, startY)
+ val end = Offset(x, endY)
+ swipe(start, end, 200)
+ }
+
+ /**
+ * The distance of a swipe's start position from the node's edge, in terms of the node's length.
+ * We do not start the swipe exactly on the node's edge, but somewhat more inward, since swiping
+ * from the exact edge may behave in an unexpected way (e.g. may open a navigation drawer).
+ */
+ private val edgeFuzzFactor = 0.083f
}
diff --git a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/ObservableThemeTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ObservableThemeTest.kt
similarity index 99%
rename from compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/ObservableThemeTest.kt
rename to compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ObservableThemeTest.kt
index 11ecac7..b3ed829 100644
--- a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/ObservableThemeTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ObservableThemeTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.ui.integration.test
+package androidx.compose.material
import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabScreenshotTest.kt
index 0ac6564..c642587 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/TabScreenshotTest.kt
@@ -21,6 +21,8 @@
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Box
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.testutils.assertAgainstGolden
@@ -298,6 +300,48 @@
)
}
+ @Test
+ fun leadingIconTabs_lightTheme_defaultColors() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(lightColors()) {
+ DefaultLeadingIconTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "leadingIconTabs_lightTheme_defaultColors"
+ )
+ }
+
+ @Test
+ fun leadingIconTabs_darkTheme_defaultColors() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ composeTestRule.setContent {
+ scope = rememberCoroutineScope()
+ MaterialTheme(darkColors()) {
+ DefaultLeadingIconTabs(interactionSource)
+ }
+ }
+
+ assertTabsMatch(
+ scope = scope!!,
+ interactionSource = interactionSource,
+ interaction = null,
+ goldenIdentifier = "leadingIconTabs_darkTheme_defaultColors"
+ )
+ }
+
/**
* Asserts that the tabs match the screenshot with identifier [goldenIdentifier].
*
@@ -410,4 +454,41 @@
}
}
+/**
+ * Default colored [TabRow] with three [LeadingIconTab]s. The first [LeadingIconTab] is selected,
+ * and the rest are not.
+ *
+ * @param interactionSource the [MutableInteractionSource] for the first [LeadingIconTab], to control its
+ * visual state.
+ */
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+private fun DefaultLeadingIconTabs(
+ interactionSource: MutableInteractionSource
+) {
+ Box(Modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
+ TabRow(selectedTabIndex = 0) {
+ LeadingIconTab(
+ text = { Text("TAB") },
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite") },
+ selected = true,
+ interactionSource = interactionSource,
+ onClick = {}
+ )
+ LeadingIconTab(
+ text = { Text("TAB") },
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite") },
+ selected = false,
+ onClick = {}
+ )
+ LeadingIconTab(
+ text = { Text("TAB") },
+ icon = { Icon(Icons.Filled.Favorite, contentDescription = "Favorite") },
+ selected = false,
+ onClick = {}
+ )
+ }
+ }
+}
+
private const val Tag = "Tab"
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 afc4989..b152c14 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
@@ -22,6 +22,7 @@
import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.samples.LeadingIconTabs
import androidx.compose.material.samples.ScrollingTextTabs
import androidx.compose.material.samples.TextTabs
import androidx.compose.runtime.Composable
@@ -35,8 +36,8 @@
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertCountEquals
@@ -132,6 +133,55 @@
.assertHasClickAction()
}
+ @OptIn(ExperimentalMaterialApi::class)
+ @Test
+ fun leadingIconTab_defaultSemantics() {
+ rule.setMaterialContent {
+ TabRow(0) {
+ LeadingIconTab(
+ text = { Text("Text") },
+ icon = { Icon(icon, null) },
+ modifier = Modifier.testTag("leadingIconTab"),
+ selected = true,
+ onClick = {}
+ )
+ }
+ }
+
+ rule.onNodeWithTag("leadingIconTab")
+ .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Tab))
+ .assertIsSelected()
+ .assertIsEnabled()
+ .assertHasClickAction()
+
+ rule.onNodeWithTag("leadingIconTab")
+ .onParent()
+ .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.SelectableGroup))
+ }
+
+ @OptIn(ExperimentalMaterialApi::class)
+ @Test
+ fun leadingIconTab_disabledSemantics() {
+ rule.setMaterialContent {
+ Box {
+ LeadingIconTab(
+ enabled = false,
+ text = { Text("Text") },
+ icon = { Icon(icon, null) },
+ modifier = Modifier.testTag("leadingIconTab"),
+ selected = true,
+ onClick = {}
+ )
+ }
+ }
+
+ rule.onNodeWithTag("leadingIconTab")
+ .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Tab))
+ .assertIsSelected()
+ .assertIsNotEnabled()
+ .assertHasClickAction()
+ }
+
@Test
fun textTab_height() {
rule
@@ -166,6 +216,23 @@
.assertHeightIsEqualTo(ExpectedLargeTabHeight)
}
+ @OptIn(ExperimentalMaterialApi::class)
+ @Test
+ fun leadingIconTab_height() {
+ rule
+ .setMaterialContentForSizeAssertions {
+ Surface {
+ LeadingIconTab(
+ text = { Text("Text") },
+ icon = { Icon(icon, null) },
+ selected = true,
+ onClick = {}
+ )
+ }
+ }
+ .assertHeightIsEqualTo(ExpectedSmallTabHeight)
+ }
+
@Test
fun fixedTabRow_indicatorPosition() {
val indicatorHeight = 1.dp
@@ -340,6 +407,47 @@
baselinePositionY.assertIsEqualTo(expectedPositionY, "baseline y-position")
}
+ @OptIn(ExperimentalMaterialApi::class)
+ @Test
+ fun LeadingIconTab_textAndIconPosition() {
+ rule.setMaterialContent {
+ Box {
+ TabRow(
+ modifier = Modifier.testTag("tabRow"),
+ selectedTabIndex = 0
+ ) {
+ LeadingIconTab(
+ text = {
+ Text("TAB", Modifier.testTag("text"))
+ },
+ icon = { Icon(Icons.Filled.Favorite, null, Modifier.testTag("icon")) },
+ selected = true,
+ onClick = {}
+ )
+ }
+ }
+ }
+
+ val tabRowBounds =
+ rule.onNodeWithTag("tabRow", useUnmergedTree = true).getUnclippedBoundsInRoot()
+
+ val textBounds =
+ rule.onNodeWithTag("text", useUnmergedTree = true).getUnclippedBoundsInRoot()
+
+ val textDistanceFromIcon = 8.dp
+
+ val iconBounds =
+ rule.onNodeWithTag("icon", useUnmergedTree = true).getUnclippedBoundsInRoot()
+ textBounds.left.assertIsEqualTo(
+ iconBounds.right + textDistanceFromIcon,
+ "textBounds left-position"
+ )
+
+ val iconOffset =
+ (tabRowBounds.width - iconBounds.width - textBounds.width - textDistanceFromIcon) / 2
+ iconBounds.left.assertIsEqualTo(iconOffset, "iconBounds left-position")
+ }
+
@Test
fun scrollableTabRow_indicatorPosition() {
val indicatorHeight = 1.dp
@@ -446,6 +554,52 @@
}
@Test
+ fun fixedLeadingIconTabRow_initialTabSelected() {
+ rule
+ .setMaterialContent {
+ LeadingIconTabs()
+ }
+
+ // Only the first tab should be selected
+ rule.onAllNodes(isSelectable())
+ .assertCountEquals(3)
+ .apply {
+ get(0).assertIsSelected()
+ get(1).assertIsNotSelected()
+ get(2).assertIsNotSelected()
+ }
+ }
+
+ @Test
+ fun LeadingIconTabRow_selectNewTab() {
+ rule
+ .setMaterialContent {
+ LeadingIconTabs()
+ }
+
+ // Only the first tab should be selected
+ rule.onAllNodes(isSelectable())
+ .assertCountEquals(3)
+ .apply {
+ get(0).assertIsSelected()
+ get(1).assertIsNotSelected()
+ get(2).assertIsNotSelected()
+ }
+
+ // Click the last tab
+ rule.onAllNodes(isSelectable())[2].performClick()
+
+ // Now only the last tab should be selected
+ rule.onAllNodes(isSelectable())
+ .assertCountEquals(3)
+ .apply {
+ get(0).assertIsNotSelected()
+ get(1).assertIsNotSelected()
+ get(2).assertIsSelected()
+ }
+ }
+
+ @Test
fun scrollableTabRow_initialTabSelected() {
rule
.setMaterialContent {
@@ -528,4 +682,29 @@
assertThat(clicks).isEqualTo(0)
}
}
+
+ @OptIn(ExperimentalMaterialApi::class)
+ @Test
+ fun leadingIconTab_disabled_noClicks() {
+ var clicks = 0
+ rule.setMaterialContent {
+ Box {
+ LeadingIconTab(
+ enabled = false,
+ text = { Text("Text") },
+ icon = { Icon(icon, null) },
+ modifier = Modifier.testTag("tab"),
+ selected = true,
+ onClick = { clicks++ }
+ )
+ }
+ }
+
+ rule.onNodeWithTag("tab")
+ .performClick()
+
+ rule.runOnIdle {
+ assertThat(clicks).isEqualTo(0)
+ }
+ }
}
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 48389f1..77c0619 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
@@ -17,6 +17,7 @@
package androidx.compose.material
import androidx.compose.animation.core.TweenSpec
+import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectTapGestures
@@ -30,15 +31,19 @@
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.semantics.dismiss
@@ -48,9 +53,9 @@
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.lerp
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
+import kotlin.math.max
import kotlin.math.roundToInt
/**
@@ -164,10 +169,10 @@
confirmStateChange = confirmStateChange
) {
/**
- * Whether the drawer is open.
+ * Whether the drawer is open, either in opened or expanded state.
*/
val isOpen: Boolean
- get() = currentValue == BottomDrawerValue.Open
+ get() = currentValue != BottomDrawerValue.Closed
/**
* Whether the drawer is closed.
@@ -183,19 +188,24 @@
/**
* Open the drawer with animation and suspend until it if fully opened or animation has been
- * cancelled. This method will throw [CancellationException] if the animation is
- * interrupted
+ * cancelled. If the content height is less than [BottomDrawerOpenFraction], the drawer state
+ * will move to [BottomDrawerValue.Expanded] instead.
*
- * @return the reason the open animation ended
+ * @throws [CancellationException] if the animation is interrupted
+ *
*/
- suspend fun open() = animateTo(BottomDrawerValue.Open)
+ suspend fun open() {
+ val targetValue =
+ if (isOpenEnabled) BottomDrawerValue.Open else BottomDrawerValue.Expanded
+ animateTo(targetValue)
+ }
/**
* Close the drawer with animation and suspend until it if fully closed or animation has been
- * cancelled. This method will throw [CancellationException] if the animation is
- * interrupted
+ * cancelled.
*
- * @return the reason the close animation ended
+ * @throws [CancellationException] if the animation is interrupted
+ *
*/
suspend fun close() = animateTo(BottomDrawerValue.Closed)
@@ -203,10 +213,14 @@
* Expand the drawer with animation and suspend until it if fully expanded or animation has
* been cancelled.
*
- * @return the reason the expand animation ended
+ * @throws [CancellationException] if the animation is interrupted
+ *
*/
suspend fun expand() = animateTo(BottomDrawerValue.Expanded)
+ private val isOpenEnabled: Boolean
+ get() = anchors.values.contains(BottomDrawerValue.Open)
+
internal val nestedScrollConnection = this.PreUpPostDownNestedScrollConnection
companion object {
@@ -377,7 +391,7 @@
* @sample androidx.compose.material.samples.BottomDrawerSample
*
* @param drawerState state of the drawer
- * @param modifier optional modifier for the drawer
+ * @param modifier optional [Modifier] for the entire component
* @param gesturesEnabled whether or not drawer can be interacted by gestures
* @param drawerShape shape of the drawer sheet
* @param drawerElevation drawer sheet elevation. This controls the size of the shadow below the
@@ -390,7 +404,6 @@
* @param scrimColor color of the scrim that obscures content when the drawer is open
* @param content content of the rest of the UI
*
- * @throws IllegalStateException when parent has [Float.POSITIVE_INFINITY] height
*/
@Composable
@ExperimentalMaterialApi
@@ -407,85 +420,77 @@
content: @Composable () -> Unit
) {
val scope = rememberCoroutineScope()
+
BoxWithConstraints(modifier.fillMaxSize()) {
- val modalDrawerConstraints = constraints
- // TODO : think about Infinite max bounds case
- if (!modalDrawerConstraints.hasBoundedHeight) {
- throw IllegalStateException("Drawer shouldn't have infinite height")
+ val fullHeight = constraints.maxHeight.toFloat()
+ var drawerHeight by remember(fullHeight) { mutableStateOf(fullHeight) }
+ // TODO(b/178630869) Proper landscape support
+ val isLandscape = constraints.maxWidth > constraints.maxHeight
+
+ val minHeight = 0f
+ val peekHeight = fullHeight * BottomDrawerOpenFraction
+ val expandedHeight = max(minHeight, fullHeight - drawerHeight)
+ val anchors = if (drawerHeight < peekHeight || isLandscape) {
+ mapOf(
+ fullHeight to BottomDrawerValue.Closed,
+ expandedHeight to BottomDrawerValue.Expanded
+ )
+ } else {
+ mapOf(
+ fullHeight to BottomDrawerValue.Closed,
+ peekHeight to BottomDrawerValue.Open,
+ expandedHeight to BottomDrawerValue.Expanded
+ )
}
- val minValue = 0f
- val maxValue = modalDrawerConstraints.maxHeight.toFloat()
-
- // TODO: add proper landscape support
- val isLandscape = modalDrawerConstraints.maxWidth > modalDrawerConstraints.maxHeight
- val openValue = if (isLandscape) minValue else lerp(
- minValue,
- maxValue,
- BottomDrawerOpenFraction
- )
- val anchors =
- if (isLandscape) {
- mapOf(
- maxValue to BottomDrawerValue.Closed,
- minValue to BottomDrawerValue.Open
- )
- } else {
- mapOf(
- maxValue to BottomDrawerValue.Closed,
- openValue to BottomDrawerValue.Open,
- minValue to BottomDrawerValue.Expanded
- )
- }
val blockClicks = if (drawerState.isClosed) {
Modifier
} else {
Modifier.pointerInput(Unit) { detectTapGestures {} }
}
- Box(
+ val drawerConstraints = with(LocalDensity.current) {
Modifier
- .nestedScroll(drawerState.nestedScrollConnection)
- .swipeable(
- state = drawerState,
- anchors = anchors,
- orientation = Orientation.Vertical,
- enabled = gesturesEnabled,
- resistance = null
+ .sizeIn(
+ maxWidth = constraints.maxWidth.toDp(),
+ maxHeight = constraints.maxHeight.toDp()
)
- ) {
- Box {
- content()
- }
- Scrim(
- open = drawerState.isOpen,
- onClose = { scope.launch { drawerState.close() } },
- fraction = {
- // as we scroll "from height to 0" , need to reverse fraction
- 1 - calculateFraction(openValue, maxValue, drawerState.offset.value)
- },
- color = scrimColor
+ }
+ val swipeable = Modifier
+ .nestedScroll(drawerState.nestedScrollConnection)
+ .swipeable(
+ state = drawerState,
+ anchors = anchors,
+ orientation = Orientation.Vertical,
+ enabled = gesturesEnabled,
+ resistance = null
+ )
+
+ Box(swipeable) {
+ content()
+ BottomDrawerScrim(
+ color = scrimColor,
+ onDismiss = { scope.launch { drawerState.close() } },
+ visible = drawerState.targetValue != BottomDrawerValue.Closed
)
Surface(
- modifier = with(LocalDensity.current) {
- Modifier.sizeIn(
- minWidth = modalDrawerConstraints.minWidth.toDp(),
- minHeight = modalDrawerConstraints.minHeight.toDp(),
- maxWidth = modalDrawerConstraints.maxWidth.toDp(),
- maxHeight = modalDrawerConstraints.maxHeight.toDp()
- )
- }
+ drawerConstraints
+ .onGloballyPositioned { position ->
+ drawerHeight = position.size.height.toFloat()
+ }
.semantics {
paneTitle = Strings.NavigationMenu
if (drawerState.isOpen) {
+ // TODO(b/180101663) The action currently doesn't return the correct results
dismiss(action = { scope.launch { drawerState.close() }; true })
}
- }.offset { IntOffset(0, drawerState.offset.value.roundToInt()) },
+ }
+ .offset { IntOffset(x = 0, y = drawerState.offset.value.roundToInt()) },
shape = drawerShape,
color = drawerBackgroundColor,
contentColor = drawerContentColor,
elevation = drawerElevation
) {
- Column(Modifier.fillMaxSize().then(blockClicks), content = drawerContent)
+ Column(blockClicks, content = drawerContent)
}
}
}
@@ -515,6 +520,35 @@
((pos - a) / (b - a)).coerceIn(0f, 1f)
@Composable
+private fun BottomDrawerScrim(
+ color: Color,
+ onDismiss: () -> Unit,
+ visible: Boolean
+) {
+ if (color != Color.Transparent) {
+ val alpha by animateFloatAsState(
+ targetValue = if (visible) 1f else 0f,
+ animationSpec = TweenSpec()
+ )
+ val dismissModifier = if (visible) {
+ Modifier.pointerInput(onDismiss) {
+ detectTapGestures { onDismiss() }
+ }
+ } else {
+ Modifier
+ }
+
+ Canvas(
+ Modifier
+ .fillMaxSize()
+ .then(dismissModifier)
+ ) {
+ drawRect(color = color, alpha = alpha)
+ }
+ }
+}
+
+@Composable
private fun Scrim(
open: Boolean,
onClose: () -> Unit,
@@ -543,4 +577,4 @@
// this is taken from the DrawerLayout's DragViewHelper as a min duration.
private val AnimationSpec = TweenSpec<Float>(durationMillis = 256)
-internal const val BottomDrawerOpenFraction = 0.5f
+private const val BottomDrawerOpenFraction = 0.5f
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 6002125..e3ca819 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
@@ -104,6 +104,8 @@
/**
* Show the bottom sheet with animation and suspend until it's shown. If half expand is
* enabled, the bottom sheet will be half expanded. Otherwise it will be fully expanded.
+ *
+ * @throws [CancellationException] if the animation is interrupted
*/
suspend fun show() {
val targetValue =
@@ -116,7 +118,7 @@
* Half expand the bottom sheet if half expand is enabled with animation and suspend until it
* animation is complete or cancelled
*
- * @return the reason the half expand animation ended
+ * @throws [CancellationException] if the animation is interrupted
*/
internal suspend fun halfExpand() {
if (!isHalfExpandedEnabled) {
@@ -127,19 +129,17 @@
/**
* Fully expand the bottom sheet with animation and suspend until it if fully expanded or
- * animation has been cancelled. This method will throw [CancellationException] if the
- * animation is interrupted
- *
- * @return the reason the expand animation ended
+ * animation has been cancelled.
+ * *
+ * @throws [CancellationException] if the animation is interrupted
*/
internal suspend fun expand() = animateTo(ModalBottomSheetValue.Expanded)
/**
* Hide the bottom sheet with animation and suspend until it if fully hidden or animation has
- * been cancelled. This method will throw [CancellationException] if the animation is
- * interrupted
+ * been cancelled.
*
- * @return the reason the hide animation ended
+ * @throws [CancellationException] if the animation is interrupted
*/
suspend fun hide() = animateTo(ModalBottomSheetValue.Hidden)
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 0d107e4..daec88e 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
@@ -26,8 +26,12 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
@@ -73,6 +77,8 @@
* @param selectedContentColor the color for the content of this tab when selected, and the color
* of the ripple.
* @param unselectedContentColor the color for the content of this tab when not selected
+ *
+ * @see LeadingIconTab
*/
@Composable
fun Tab(
@@ -106,6 +112,74 @@
}
/**
+ * A LeadingIconTab represents a single page of content using a text label and an icon in
+ * front of the label.
+ * It represents its selected state by tinting the text label and icon with [selectedContentColor].
+ *
+ * This should typically be used inside of a [TabRow], see the corresponding documentation for
+ * example usage.
+ *
+ * @param selected whether this tab is selected or not
+ * @param onClick the callback to be invoked when this tab is selected
+ * @param text the text label displayed in this tab
+ * @param icon the icon displayed in this tab
+ * @param modifier optional [Modifier] for this tab
+ * @param enabled controls the enabled state of this tab. When `false`, this tab will not
+ * be clickable and will appear disabled to accessibility services.
+ * @param interactionSource the [MutableInteractionSource] representing the different [Interaction]s
+ * present on this tab. You can create and pass in your own remembered [MutableInteractionSource] if
+ * you want to read the [Interaction] and customize the appearance / behavior of this tab
+ * in different [Interaction]s.
+ * @param selectedContentColor the color for the content of this tab when selected, and the color
+ * of the ripple.
+ * @param unselectedContentColor the color for the content of this tab when not selected
+ *
+ * @see Tab
+ */
+@ExperimentalMaterialApi
+@Composable
+fun LeadingIconTab(
+ selected: Boolean,
+ onClick: () -> Unit,
+ text: @Composable (() -> Unit),
+ icon: @Composable (() -> Unit),
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ selectedContentColor: Color = LocalContentColor.current,
+ unselectedContentColor: Color = selectedContentColor.copy(alpha = ContentAlpha.medium)
+) {
+ // The color of the Ripple should always the be selected color, as we want to show the color
+ // before the item is considered selected, and hence before the new contentColor is
+ // provided by TabTransition.
+ val ripple = rememberRipple(bounded = false, color = selectedContentColor)
+
+ TabTransition(selectedContentColor, unselectedContentColor, selected) {
+ Row(
+ modifier = modifier
+ .height(SmallTabHeight)
+ .selectable(
+ selected = selected,
+ onClick = onClick,
+ enabled = enabled,
+ role = Role.Tab,
+ interactionSource = interactionSource,
+ indication = ripple
+ )
+ .padding(horizontal = HorizontalTextPadding)
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ icon()
+ Spacer(Modifier.requiredWidth(TextDistanceFromLeadingIcon))
+ val style = MaterialTheme.typography.button.copy(textAlign = TextAlign.Center)
+ ProvideTextStyle(style, content = text)
+ }
+ }
+}
+
+/**
* Generic [Tab] overload that is not opinionated about content / color. See the other overload
* for a Tab that has specific slots for text and / or an icon, as well as providing the correct
* colors for selected / unselected states.
@@ -372,3 +446,5 @@
private val DoubleLineTextBaselineWithIcon = 6.dp
// Distance from the first text baseline to the bottom of the icon in a combined tab
private val IconDistanceFromBaseline = 20.sp
+// Distance from the end of the leading icon to the start of the text
+private val TextDistanceFromLeadingIcon = 8.dp
diff --git a/compose/runtime/runtime-lint/build.gradle b/compose/runtime/runtime-lint/build.gradle
index aa6a9b5..751acb3 100644
--- a/compose/runtime/runtime-lint/build.gradle
+++ b/compose/runtime/runtime-lint/build.gradle
@@ -24,6 +24,28 @@
id("kotlin")
}
+// New configuration that allows us to specify a dependency we will include into the resulting
+// jar, since dependencies aren't currently allowed in lint projects included via lintPublish
+// (b/182319899)
+configurations {
+ bundleWithJar
+ testImplementation.extendsFrom bundleWithJar
+ compileOnly.extendsFrom bundleWithJar
+}
+
+jar {
+ dependsOn configurations.bundleWithJar
+ from {
+ configurations.bundleWithJar
+ // The stdlib is already bundled with lint, so no need to include it manually in
+ // the lint.jar
+ .filter( { !(it.name =~ /kotlin-stdlib.*\.jar/ )})
+ .collect {
+ it.isDirectory() ? it : zipTree(it)
+ }
+ }
+}
+
dependencies {
// compileOnly because we use lintChecks and it doesn't allow other types of deps
// this ugly hack exists because of b/63873667
@@ -33,6 +55,7 @@
compileOnly(LINT_API_MIN)
}
compileOnly(KOTLIN_STDLIB)
+ bundleWithJar(KOTLIN_METADATA_JVM)
testImplementation(KOTLIN_STDLIB)
testImplementation(LINT_CORE)
diff --git a/compose/runtime/runtime-lint/lint-baseline.xml b/compose/runtime/runtime-lint/lint-baseline.xml
deleted file mode 100644
index 297ae16..0000000
--- a/compose/runtime/runtime-lint/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" version="4.2.0-beta02">
-
-</issues>
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableCoroutineCreationDetector.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableCoroutineCreationDetector.kt
new file mode 100644
index 0000000..2617bdf
--- /dev/null
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableCoroutineCreationDetector.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.runtime.lint
+
+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.PsiMethod
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
+import org.jetbrains.uast.kotlin.KotlinULambdaExpression
+import org.jetbrains.uast.kotlin.declarations.KotlinUMethod
+import java.util.EnumSet
+
+/**
+ * [Detector] that checks `async` and `launch` calls to make sure they don't happen inside the
+ * body of a composable function / lambda.
+ */
+class ComposableCoroutineCreationDetector : Detector(), SourceCodeScanner {
+ override fun getApplicableMethodNames() = listOf(AsyncShortName, LaunchShortName)
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ val packageName = (method.containingFile as? PsiJavaFile)?.packageName
+ if (packageName != CoroutinePackageName) return
+ val name = method.name
+
+ var expression: UElement? = node
+
+ // Limit the search depth in case of an error - in most cases the depth should be
+ // fairly shallow unless there are many if / else / while statements.
+ var depth = 0
+
+ // Find the parent function / lambda this call expression is inside
+ while (depth < 10) {
+ expression = expression?.uastParent
+
+ // TODO: this won't handle inline functions, but we also don't know if they are
+ // inline when they are defined in bytecode because this information doesn't
+ // exist in PSI. If this information isn't added to PSI / UAST, we would need to
+ // manually parse the @Metadata annotation.
+ when (expression) {
+ // In the body of a lambda
+ is KotlinULambdaExpression -> {
+ if (expression.isComposable) {
+ context.report(
+ CoroutineCreationDuringComposition,
+ node,
+ context.getNameLocation(node),
+ "Calls to $name should happen inside a LaunchedEffect and " +
+ "not composition"
+ )
+ return
+ }
+ val parent = expression.uastParent
+ if (parent is KotlinUFunctionCallExpression && parent.isDeclarationInline) {
+ // We are now in a non-composable lambda parameter inside an inline function
+ // For example, a scoping function such as run {} or apply {} - since the
+ // body will be inlined and this is a common case, try to see if there is
+ // a parent composable function above us, since it is still most likely
+ // an error to call these methods inside an inline function, inside a
+ // Composable function.
+ continue
+ } else {
+ return
+ }
+ }
+ // In the body of a function
+ is KotlinUMethod -> {
+ if (expression.hasAnnotation("androidx.compose.runtime.Composable")) {
+ context.report(
+ CoroutineCreationDuringComposition,
+ node,
+ context.getNameLocation(node),
+ "Calls to $name should happen inside a LaunchedEffect and " +
+ "not composition"
+ )
+ }
+ return
+ }
+ }
+ depth++
+ }
+ }
+
+ companion object {
+ val CoroutineCreationDuringComposition = Issue.create(
+ "CoroutineCreationDuringComposition",
+ "Calls to `async` or `launch` should happen inside a LaunchedEffect and not " +
+ "composition",
+ "Creating a coroutine with `async` or `launch` during composition is often incorrect " +
+ "- this means that a coroutine will be created even if the composition fails / is" +
+ " rolled back, and it also means that multiple coroutines could end up mutating " +
+ "the same state, causing inconsistent results. Instead, use `LaunchedEffect` and " +
+ "create coroutines inside the suspending block. The block will only run after a " +
+ "successful composition, and will cancel existing coroutines when `key` changes, " +
+ "allowing correct cleanup.",
+ Category.CORRECTNESS, 3, Severity.ERROR,
+ Implementation(
+ ComposableCoroutineCreationDetector::class.java,
+ EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
+ )
+ )
+ }
+}
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 4e93399..85272a1 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
@@ -74,8 +74,9 @@
val typeReference = ktParameter.typeReference!!
- // Ideally this annotation should be available on the PsiType itself
- // https://youtrack.jetbrains.com/issue/KT-45244
+ // Currently type annotations don't appear on the psiType in the version of
+ // UAST / PSI we are using, so we have to look through the type reference.
+ // Should be fixed when Lint upgrades the version to 1.4.30+.
val hasComposableAnnotationOnType = typeReference.annotationEntries.any {
(it.toUElement() as UAnnotation).qualifiedName == ComposableFqn
}
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 a860b1a..b550b50 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
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("UnstableApiUsage")
+
package androidx.compose.runtime.lint
import com.android.tools.lint.client.api.IssueRegistry
@@ -27,10 +29,11 @@
override val api = 8
override val minApi = CURRENT_API
override val issues get() = listOf(
- CompositionLocalNamingDetector.CompositionLocalNaming,
+ ComposableCoroutineCreationDetector.CoroutineCreationDuringComposition,
ComposableLambdaParameterDetector.ComposableLambdaParameterNaming,
ComposableLambdaParameterDetector.ComposableLambdaParameterPosition,
ComposableNamingDetector.ComposableNaming,
+ CompositionLocalNamingDetector.CompositionLocalNaming,
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 6d8f4d6..6d78e36 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
@@ -16,18 +16,234 @@
package androidx.compose.runtime.lint
+import com.intellij.lang.jvm.annotation.JvmAnnotationArrayValue
+import com.intellij.lang.jvm.annotation.JvmAnnotationAttributeValue
+import com.intellij.lang.jvm.annotation.JvmAnnotationConstantValue
+import com.intellij.psi.PsiAnnotation
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.impl.compiled.ClsMemberImpl
+import com.intellij.psi.impl.compiled.ClsMethodImpl
+import com.intellij.psi.impl.compiled.ClsParameterImpl
+import com.intellij.psi.util.ClassUtil
+import kotlinx.metadata.Flag
+import kotlinx.metadata.KmDeclarationContainer
+import kotlinx.metadata.KmFunction
+import kotlinx.metadata.jvm.KotlinClassHeader
+import kotlinx.metadata.jvm.KotlinClassMetadata
+import kotlinx.metadata.jvm.annotations
+import kotlinx.metadata.jvm.signature
+import org.jetbrains.kotlin.lexer.KtTokens.INLINE_KEYWORD
+import org.jetbrains.kotlin.psi.KtNamedFunction
+import org.jetbrains.kotlin.psi.KtParameter
+import org.jetbrains.kotlin.psi.KtTypeReference
+import org.jetbrains.uast.UAnnotation
+import org.jetbrains.uast.ULambdaExpression
import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UParameter
+import org.jetbrains.uast.getContainingUMethod
+import org.jetbrains.uast.getParameterForArgument
+import org.jetbrains.uast.kotlin.AbstractKotlinUVariable
+import org.jetbrains.uast.kotlin.KotlinUFunctionCallExpression
+import org.jetbrains.uast.resolveToUElement
+import org.jetbrains.uast.toUElement
// TODO: KotlinUMethodWithFakeLightDelegate.hasAnnotation() returns null for some reason, so just
// look at the annotations directly
// TODO: annotations is deprecated but the replacement uAnnotations isn't available on the
// version of lint / uast we compile against
+/**
+ * Returns whether this method is @Composable or not
+ */
@Suppress("DEPRECATION")
-val UMethod.isComposable get() = annotations.any { it.qualifiedName == ComposableFqn }
+val UMethod.isComposable
+ get() = annotations.any { it.qualifiedName == ComposableFqn }
+
+/**
+ * Returns whether this parameter's type is @Composable or not
+ */
+val UParameter.isComposable: Boolean
+ get() = when (val source = sourcePsi) {
+ // The parameter is defined in Kotlin source
+ is KtParameter -> source.typeReference!!.isComposable
+ // The parameter is in a class file. Currently type annotations aren't added to the
+ // underlying type (https://youtrack.jetbrains.com/issue/KT-45307), so instead we use the
+ // metadata annotation.
+ is ClsParameterImpl -> {
+ // Find the containing method, so we can get metadata from the containing class
+ val containingMethod = getContainingUMethod()!!.sourcePsi as ClsMethodImpl
+ val declarationContainer = containingMethod.getKmDeclarationContainer()
+ val kmFunction = declarationContainer?.findKmFunctionForPsiMethod(containingMethod)
+
+ val kmValueParameter = kmFunction?.valueParameters?.find {
+ it.name == name
+ }
+
+ kmValueParameter?.type?.annotations?.find {
+ it.className == KmComposableFqn
+ } != null
+ }
+ // The parameter is in Java source / other, ignore
+ else -> false
+ }
+
+/**
+ * Returns whether this type reference is @Composable or not
+ */
+val KtTypeReference.isComposable: Boolean
+ // This annotation should be available on the PsiType itself in 1.4.30+, but we are
+ // currently on an older version of UAST / Kotlin embedded compiled
+ // (https://youtrack.jetbrains.com/issue/KT-45244)
+ get() = annotationEntries.any {
+ (it.toUElement() as UAnnotation).qualifiedName == ComposableFqn
+ }
+
+/**
+ * Returns whether this lambda expression is @Composable or not
+ */
+val ULambdaExpression.isComposable: Boolean
+ get() {
+ when (val lambdaParent = uastParent) {
+ // Function call with a lambda parameter
+ is KotlinUFunctionCallExpression -> {
+ val parameter = lambdaParent.getParameterForArgument(this) ?: return false
+ if (!(parameter.toUElement() as UParameter).isComposable) return false
+ }
+ // A local / non-local lambda variable
+ is AbstractKotlinUVariable -> {
+ val hasComposableAnnotationOnLambda = findAnnotation(ComposableFqn) != null
+ val hasComposableAnnotationOnType =
+ (lambdaParent.typeReference?.sourcePsi as? KtTypeReference)
+ ?.isComposable == true
+
+ if (!hasComposableAnnotationOnLambda && !hasComposableAnnotationOnType) return false
+ }
+ // This probably shouldn't be called, but safe return in case a new UAST type is added
+ // in the future
+ else -> return false
+ }
+ return true
+ }
+
+/**
+ * @return whether the resolved declaration for this call expression is an inline function
+ */
+val KotlinUFunctionCallExpression.isDeclarationInline: Boolean
+ get() {
+ return when (val source = resolveToUElement()?.sourcePsi) {
+ // Parsing a method defined in a class file
+ is ClsMethodImpl -> {
+ val declarationContainer = source.getKmDeclarationContainer()
+
+ val flags = declarationContainer
+ ?.findKmFunctionForPsiMethod(source)?.flags ?: return false
+ return Flag.Function.IS_INLINE(flags)
+ }
+ // Parsing a method defined in Kotlin source
+ is KtNamedFunction -> {
+ source.hasModifier(INLINE_KEYWORD)
+ }
+ // Parsing something else (such as a property) which cannot be inline
+ else -> false
+ }
+ }
+
+// TODO: https://youtrack.jetbrains.com/issue/KT-45310
+// Currently there is no built in support for parsing kotlin metadata from kotlin class files, so
+// we need to manually inspect the annotations and work with Cls* (compiled PSI).
+/**
+ * Returns the [KmDeclarationContainer] using the kotlin.Metadata annotation present on the
+ * surrounding class. Returns null if there is no surrounding annotation (not parsing a Kotlin
+ * class file), the annotation data is for an unsupported version of Kotlin, or if the metadata
+ * represents a synthetic
+ */
+private fun ClsMemberImpl<*>.getKmDeclarationContainer(): KmDeclarationContainer? {
+ val classKotlinMetadataAnnotation = containingClass?.annotations?.find {
+ // hasQualifiedName() not available on the min version of Lint we compile against
+ it.qualifiedName == KotlinMetadataFqn
+ } ?: return null
+
+ val metadata = KotlinClassMetadata.read(classKotlinMetadataAnnotation.toHeader())
+ ?: return null
+
+ return when (metadata) {
+ is KotlinClassMetadata.Class -> metadata.toKmClass()
+ is KotlinClassMetadata.FileFacade -> metadata.toKmPackage()
+ is KotlinClassMetadata.SyntheticClass -> null
+ is KotlinClassMetadata.MultiFileClassFacade -> null
+ is KotlinClassMetadata.MultiFileClassPart -> metadata.toKmPackage()
+ is KotlinClassMetadata.Unknown -> null
+ }
+}
+
+/**
+ * Returns a [KotlinClassHeader] by parsing the attributes of this @kotlin.Metadata annotation.
+ *
+ * See: https://github.com/udalov/kotlinx-metadata-examples/blob/master/src/main/java
+ * /examples/FindKotlinGeneratedMethods.java
+ */
+private fun PsiAnnotation.toHeader(): KotlinClassHeader {
+ val attributes = attributes.associate { it.attributeName to it.attributeValue }
+
+ fun JvmAnnotationAttributeValue.parseString(): String =
+ (this as JvmAnnotationConstantValue).constantValue as String
+
+ fun JvmAnnotationAttributeValue.parseInt(): Int =
+ (this as JvmAnnotationConstantValue).constantValue as Int
+
+ fun JvmAnnotationAttributeValue.parseStringArray(): Array<String> =
+ (this as JvmAnnotationArrayValue).values.map {
+ it.parseString()
+ }.toTypedArray()
+
+ fun JvmAnnotationAttributeValue.parseIntArray(): IntArray =
+ (this as JvmAnnotationArrayValue).values.map {
+ it.parseInt()
+ }.toTypedArray().toIntArray()
+
+ val kind = attributes["k"]?.parseInt()
+ val metadataVersion = attributes["mv"]?.parseIntArray()
+ val bytecodeVersion = attributes["bv"]?.parseIntArray()
+ val data1 = attributes["d1"]?.parseStringArray()
+ val data2 = attributes["d2"]?.parseStringArray()
+ val extraString = attributes["xs"]?.parseString()
+ val packageName = attributes["pn"]?.parseString()
+ val extraInt = attributes["xi"]?.parseInt()
+
+ return KotlinClassHeader(
+ kind,
+ metadataVersion,
+ bytecodeVersion,
+ data1,
+ data2,
+ extraString,
+ packageName,
+ extraInt
+ )
+}
+
+/**
+ * @return the corresponding [KmFunction] in [this] for the given [method], matching by name and
+ * signature.
+ */
+private fun KmDeclarationContainer.findKmFunctionForPsiMethod(method: PsiMethod): KmFunction? {
+ val expectedName = method.name
+ val expectedSignature = ClassUtil.getAsmMethodSignature(method)
+
+ return functions.find {
+ it.name == expectedName && it.signature?.desc == expectedSignature
+ }
+}
const val RuntimePackageName = "androidx.compose.runtime"
const val ComposableFqn = "$RuntimePackageName.Composable"
-val ComposableShortName = ComposableFqn.split(".").last()
+// kotlinx.metadata represents separators as `/` instead of `.`
+val KmComposableFqn get() = ComposableFqn.replace(".", "/")
const val RememberShortName = "remember"
+
+const val CoroutinePackageName = "kotlinx.coroutines"
+const val AsyncShortName = "async"
+const val LaunchShortName = "launch"
+
+private const val KotlinMetadataFqn = "kotlin.Metadata"
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableCoroutineCreationDetectorTest.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableCoroutineCreationDetectorTest.kt
new file mode 100644
index 0000000..bd2f0bb
--- /dev/null
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableCoroutineCreationDetectorTest.kt
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file: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)
+
+// TODO: add tests for methods defined in class files when we update Lint to support bytecode()
+// test files
+
+/**
+ * Test for [ComposableCoroutineCreationDetector].
+ */
+class ComposableCoroutineCreationDetectorTest : LintDetectorTest() {
+ override fun getDetector(): Detector = ComposableCoroutineCreationDetector()
+
+ override fun getIssues(): MutableList<Issue> =
+ mutableListOf(ComposableCoroutineCreationDetector.CoroutineCreationDuringComposition)
+
+ @Test
+ fun errors() {
+ lint().files(
+ kotlin(
+ """
+ package androidx.compose.runtime.foo
+
+ import androidx.compose.runtime.Composable
+ import kotlinx.coroutines.*
+
+ @Composable
+ fun Test() {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ }
+
+ val lambda = @Composable {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ }
+
+ val lambda2: @Composable () -> Unit = {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ }
+
+ @Composable
+ fun LambdaParameter(content: @Composable () -> Unit) {}
+
+ @Composable
+ fun Test2() {
+ LambdaParameter(content = {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ })
+ LambdaParameter {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ }
+ }
+
+ fun test3() {
+ val localLambda1 = @Composable {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ }
+
+ val localLambda2: @Composable () -> Unit = {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ }
+ }
+ """
+ ),
+ composableStub,
+ coroutineBuildersStub
+ )
+ .run()
+ .expect(
+ """
+src/androidx/compose/runtime/foo/test.kt:9: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.async {}
+ ~~~~~
+src/androidx/compose/runtime/foo/test.kt:10: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.launch {}
+ ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:14: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.async {}
+ ~~~~~
+src/androidx/compose/runtime/foo/test.kt:15: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.launch {}
+ ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:19: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.async {}
+ ~~~~~
+src/androidx/compose/runtime/foo/test.kt:20: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.launch {}
+ ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:29: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.async {}
+ ~~~~~
+src/androidx/compose/runtime/foo/test.kt:30: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.launch {}
+ ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:33: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.async {}
+ ~~~~~
+src/androidx/compose/runtime/foo/test.kt:34: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.launch {}
+ ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:40: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.async {}
+ ~~~~~
+src/androidx/compose/runtime/foo/test.kt:41: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.launch {}
+ ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:45: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.async {}
+ ~~~~~
+src/androidx/compose/runtime/foo/test.kt:46: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.launch {}
+ ~~~~~~
+14 errors, 0 warnings
+ """
+ )
+ }
+
+ @Test
+ fun errors_inlineFunctions() {
+ lint().files(
+ kotlin(
+ """
+ package androidx.compose.runtime.foo
+
+ import androidx.compose.runtime.Composable
+ import kotlinx.coroutines.*
+
+ @Composable
+ fun Test() {
+ run {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ }
+ }
+
+ val lambda = @Composable {
+ run {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ }
+ }
+
+ val lambda2: @Composable () -> Unit = {
+ run {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ }
+ }
+
+ @Composable
+ fun LambdaParameter(content: @Composable () -> Unit) {}
+
+ @Composable
+ fun Test2() {
+ LambdaParameter(content = {
+ run {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ }
+ })
+ LambdaParameter {
+ run {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ }
+ }
+ }
+
+ fun test3() {
+ val localLambda1 = @Composable {
+ run {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ }
+ }
+
+ val localLambda2: @Composable () -> Unit = {
+ run {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ }
+ }
+ }
+ """
+ ),
+ composableStub,
+ coroutineBuildersStub
+ )
+ .run()
+ .expect(
+ """
+src/androidx/compose/runtime/foo/test.kt:10: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.async {}
+ ~~~~~
+src/androidx/compose/runtime/foo/test.kt:11: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.launch {}
+ ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:17: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.async {}
+ ~~~~~
+src/androidx/compose/runtime/foo/test.kt:18: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.launch {}
+ ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:24: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.async {}
+ ~~~~~
+src/androidx/compose/runtime/foo/test.kt:25: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.launch {}
+ ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:36: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.async {}
+ ~~~~~
+src/androidx/compose/runtime/foo/test.kt:37: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.launch {}
+ ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:42: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.async {}
+ ~~~~~
+src/androidx/compose/runtime/foo/test.kt:43: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.launch {}
+ ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:51: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.async {}
+ ~~~~~
+src/androidx/compose/runtime/foo/test.kt:52: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.launch {}
+ ~~~~~~
+src/androidx/compose/runtime/foo/test.kt:58: Error: Calls to async should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.async {}
+ ~~~~~
+src/androidx/compose/runtime/foo/test.kt:59: Error: Calls to launch should happen inside a LaunchedEffect and not composition [CoroutineCreationDuringComposition]
+ CoroutineScope.launch {}
+ ~~~~~~
+14 errors, 0 warnings
+ """
+ )
+ }
+
+ @Test
+ fun noErrors() {
+ lint().files(
+ kotlin(
+ """
+ package androidx.compose.runtime.foo
+
+ import androidx.compose.runtime.Composable
+ import kotlinx.coroutines.*
+
+ fun test() {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ }
+
+ val lambda = {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ }
+
+ val lambda2: () -> Unit = {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ }
+
+ fun lambdaParameter(action: () -> Unit) {}
+
+ fun test2() {
+ lambdaParameter(action = {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ })
+ lambdaParameter {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ }
+ }
+
+ fun test3() {
+ val localLambda1 = {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ }
+
+ val localLambda2: () -> Unit = {
+ CoroutineScope.async {}
+ CoroutineScope.launch {}
+ }
+ }
+ """
+ ),
+ composableStub,
+ coroutineBuildersStub
+ )
+ .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 9c086d2..83372e1 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
@@ -73,3 +73,19 @@
): V = calculation()
"""
)
+
+val coroutineBuildersStub: LintDetectorTest.TestFile = LintDetectorTest.kotlin(
+ """
+ package kotlinx.coroutines
+
+ object CoroutineScope
+
+ fun CoroutineScope.async(
+ block: suspend CoroutineScope.() -> Unit
+ ) {}
+
+ fun CoroutineScope.launch(
+ block: suspend CoroutineScope.() -> Unit
+ ) {}
+ """
+)
diff --git a/compose/runtime/runtime-livedata/lint-baseline.xml b/compose/runtime/runtime-livedata/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/runtime/runtime-livedata/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/runtime/runtime-livedata/samples/lint-baseline.xml b/compose/runtime/runtime-livedata/samples/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/runtime/runtime-livedata/samples/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/runtime/runtime-rxjava2/lint-baseline.xml b/compose/runtime/runtime-rxjava2/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/runtime/runtime-rxjava2/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/runtime/runtime-rxjava2/samples/lint-baseline.xml b/compose/runtime/runtime-rxjava2/samples/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/runtime/runtime-rxjava2/samples/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/runtime/runtime-rxjava3/lint-baseline.xml b/compose/runtime/runtime-rxjava3/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/runtime/runtime-rxjava3/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/runtime/runtime-rxjava3/samples/lint-baseline.xml b/compose/runtime/runtime-rxjava3/samples/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/runtime/runtime-rxjava3/samples/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/runtime/runtime-saveable/lint-baseline.xml b/compose/runtime/runtime-saveable/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/runtime/runtime-saveable/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/runtime/runtime-saveable/samples/lint-baseline.xml b/compose/runtime/runtime-saveable/samples/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/runtime/runtime-saveable/samples/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/runtime/runtime-saveable/samples/src/main/java/androidx/compose/runtime/saveable/samples/SaveableStateHolderSamples.kt b/compose/runtime/runtime-saveable/samples/src/main/java/androidx/compose/runtime/saveable/samples/SaveableStateHolderSamples.kt
index 8c5feb7..d3bdb3b 100644
--- a/compose/runtime/runtime-saveable/samples/src/main/java/androidx/compose/runtime/saveable/samples/SaveableStateHolderSamples.kt
+++ b/compose/runtime/runtime-saveable/samples/src/main/java/androidx/compose/runtime/saveable/samples/SaveableStateHolderSamples.kt
@@ -46,13 +46,13 @@
modifier: Modifier = Modifier,
content: @Composable (T) -> Unit
) {
- // create RestorableStateHolder.
- val restorableStateHolder = rememberSaveableStateHolder()
- // wrap the content representing the `screen` key inside `SaveableStateProvider`.
- // you can add screen switch animations where during the animation multiple screens
- // will displayed at the same time.
+ // create SaveableStateHolder.
+ val saveableStateHolder = rememberSaveableStateHolder()
Box(modifier) {
- restorableStateHolder.SaveableStateProvider(currentScreen) {
+ // Wrap the content representing the `currentScreen` inside `SaveableStateProvider`.
+ // Here you can also add a screen switch animation like Crossfade where during the
+ // animation multiple screens will be displayed at the same time.
+ saveableStateHolder.SaveableStateProvider(currentScreen) {
content(currentScreen)
}
}
diff --git a/compose/runtime/runtime/api/1.0.0-beta02.txt b/compose/runtime/runtime/api/1.0.0-beta02.txt
index a1c11df..d73a573 100644
--- a/compose/runtime/runtime/api/1.0.0-beta02.txt
+++ b/compose/runtime/runtime/api/1.0.0-beta02.txt
@@ -456,6 +456,9 @@
method public static boolean isLiveLiteralsEnabled();
}
+ public final class ThreadMapKt {
+ }
+
}
package androidx.compose.runtime.snapshots {
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index a1c11df..d73a573 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -456,6 +456,9 @@
method public static boolean isLiveLiteralsEnabled();
}
+ public final class ThreadMapKt {
+ }
+
}
package androidx.compose.runtime.snapshots {
diff --git a/compose/runtime/runtime/api/public_plus_experimental_1.0.0-beta02.txt b/compose/runtime/runtime/api/public_plus_experimental_1.0.0-beta02.txt
index f900999d..c803652 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_1.0.0-beta02.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_1.0.0-beta02.txt
@@ -544,6 +544,9 @@
property public abstract int parameters;
}
+ public final class ThreadMapKt {
+ }
+
}
package androidx.compose.runtime.snapshots {
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index f900999d..c803652 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -544,6 +544,9 @@
property public abstract int parameters;
}
+ public final class ThreadMapKt {
+ }
+
}
package androidx.compose.runtime.snapshots {
diff --git a/compose/runtime/runtime/api/restricted_1.0.0-beta02.txt b/compose/runtime/runtime/api/restricted_1.0.0-beta02.txt
index a334aa4..d80e38d 100644
--- a/compose/runtime/runtime/api/restricted_1.0.0-beta02.txt
+++ b/compose/runtime/runtime/api/restricted_1.0.0-beta02.txt
@@ -483,6 +483,9 @@
method public static boolean isLiveLiteralsEnabled();
}
+ public final class ThreadMapKt {
+ }
+
}
package androidx.compose.runtime.snapshots {
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index a334aa4..d80e38d 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -483,6 +483,9 @@
method public static boolean isLiveLiteralsEnabled();
}
+ public final class ThreadMapKt {
+ }
+
}
package androidx.compose.runtime.snapshots {
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/lint-baseline.xml b/compose/runtime/runtime/compose-runtime-benchmark/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/runtime/runtime/compose-runtime-benchmark/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/runtime/runtime/integration-tests/lint-baseline.xml b/compose/runtime/runtime/integration-tests/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/runtime/runtime/integration-tests/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/runtime/runtime/lint-baseline.xml b/compose/runtime/runtime/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/runtime/runtime/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/runtime/runtime/samples/lint-baseline.xml b/compose/runtime/runtime/samples/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/runtime/runtime/samples/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt
index 2189a52..91193ee 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt
@@ -26,6 +26,20 @@
internal fun <T> ThreadLocal() = ThreadLocal<T?> { null }
+/**
+ * This is similar to a [ThreadLocal] but has lower overhead because it avoids a weak reference.
+ * This should only be used when the writes are delimited by a try...finally call that will clean
+ * up the reference such as [androidx.compose.runtime.snapshots.Snapshot.enter] else the reference
+ * could get pinned by the thread local causing a leak.
+ *
+ * [ThreadLocal] can be used to implement the actual for platforms that do not exhibit the same
+ * overhead for thread locals as the JVM and ART.
+ */
+internal expect class SnapshotThreadLocal<T>() {
+ fun get(): T?
+ fun set(value: T?)
+}
+
internal expect fun identityHashCode(instance: Any?): Int
@PublishedApi
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index 2f73c00..2927d96 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -20,7 +20,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.InternalComposeApi
-import androidx.compose.runtime.ThreadLocal
+import androidx.compose.runtime.SnapshotThreadLocal
import androidx.compose.runtime.synchronized
/**
@@ -1355,7 +1355,10 @@
*/
private const val INVALID_SNAPSHOT = 0
-private val threadSnapshot = ThreadLocal<Snapshot>()
+/**
+ * Current thread snapshot
+ */
+private val threadSnapshot = SnapshotThreadLocal<Snapshot>()
// A global synchronization object. This synchronization object should be taken before modifying any
// of the fields below.
diff --git a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.jvm.kt b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.jvm.kt
index 7aa0d96..63dbcb1 100644
--- a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.jvm.kt
+++ b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.jvm.kt
@@ -16,6 +16,9 @@
package androidx.compose.runtime
+import androidx.compose.runtime.internal.ThreadMap
+import androidx.compose.runtime.internal.emptyThreadMap
+
internal actual typealias AtomicReference<V> = java.util.concurrent.atomic.AtomicReference<V>
internal actual open class ThreadLocal<T> actual constructor(
@@ -35,6 +38,23 @@
}
}
+internal actual class SnapshotThreadLocal<T> {
+ private val map = AtomicReference<ThreadMap>(emptyThreadMap)
+ private val writeMutex = Any()
+
+ @Suppress("UNCHECKED_CAST")
+ actual fun get(): T? = map.get().get(Thread.currentThread().id) as T?
+
+ actual fun set(value: T?) {
+ val key = Thread.currentThread().id
+ synchronized(writeMutex) {
+ val current = map.get()
+ if (current.trySet(key, value)) return
+ map.set(current.newWith(key, value))
+ }
+ }
+}
+
internal actual fun identityHashCode(instance: Any?): Int = System.identityHashCode(instance)
@PublishedApi
diff --git a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/internal/ThreadMap.kt b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/internal/ThreadMap.kt
new file mode 100644
index 0000000..d4645eb
--- /dev/null
+++ b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/internal/ThreadMap.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime.internal
+
+internal class ThreadMap(
+ private val size: Int,
+ private val keys: LongArray,
+ private val values: Array<Any?>
+) {
+ fun get(key: Long): Any? {
+ val index = find(key)
+ return if (index >= 0) values[index] else null
+ }
+
+ /**
+ * Set the value if it is already in the map. Otherwise a new map must be allocated to contain
+ * the new entry.
+ */
+ fun trySet(key: Long, value: Any?): Boolean {
+ val index = find(key)
+ if (index < 0) return false
+ values[index] = value
+ return true
+ }
+
+ fun newWith(key: Long, value: Any?): ThreadMap {
+ val size = size
+ val newSize = values.count { it != null } + 1
+ val newKeys = LongArray(newSize)
+ val newValues = arrayOfNulls<Any?>(newSize)
+ if (newSize > 1) {
+ var dest = 0
+ var source = 0
+ while (dest < newSize && source < size) {
+ val oldKey = keys[source]
+ val oldValue = values[source]
+ if (oldKey > key) {
+ newKeys[dest] = key
+ newValues[dest] = value
+ dest++
+ // Continue with a loop without this check
+ break
+ }
+ if (oldValue != null) {
+ newKeys[dest] = oldKey
+ newValues[dest] = oldValue
+ dest++
+ }
+ source++
+ }
+ if (source == size) {
+ // Appending a value to the end.
+ newKeys[newSize - 1] = key
+ newValues[newSize - 1] = value
+ } else {
+ while (dest < newSize) {
+ val oldKey = keys[source]
+ val oldValue = values[source]
+ if (oldValue != null) {
+ newKeys[dest] = oldKey
+ newValues[dest] = oldValue
+ dest++
+ }
+ source++
+ }
+ }
+ } else {
+ // The only element
+ newKeys[0] = key
+ newValues[0] = value
+ }
+ return ThreadMap(newSize, newKeys, newValues)
+ }
+
+ private fun find(key: Long): Int {
+ var high = size - 1
+ when (high) {
+ -1 -> return -1
+ 0 -> return if (keys[0] == key) 0 else if (keys[0] > key) -2 else -1
+ }
+ var low = 0
+
+ while (low <= high) {
+ val mid = (low + high).ushr(1)
+ val midVal = keys[mid]
+ val comparison = midVal - key
+ when {
+ comparison < 0 -> low = mid + 1
+ comparison > 0 -> high = mid - 1
+ else -> return mid
+ }
+ }
+ return -(low + 1)
+ }
+}
+
+internal val emptyThreadMap = ThreadMap(0, LongArray(0), emptyArray())
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotThreadMapTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotThreadMapTests.kt
new file mode 100644
index 0000000..a50c9a8
--- /dev/null
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/snapshots/SnapshotThreadMapTests.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime.snapshots
+
+import androidx.compose.runtime.SnapshotThreadLocal
+import androidx.compose.runtime.internal.ThreadMap
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+/**
+ * Test the internal ThreadMap
+ */
+class SnapshotThreadMapTests {
+ @Test
+ fun canCreateAMap() {
+ val map = emptyThreadMap()
+ assertNotNull(map)
+ }
+
+ @Test
+ fun setOfEmptyFails() {
+ val map = emptyThreadMap()
+ val added = map.trySet(1, 1)
+ assertFalse(added)
+ }
+
+ @Test
+ fun canAddOneToEmpty() {
+ val map = emptyThreadMap()
+ val newMap = map.newWith(1, 1)
+ assertNotEquals(map, newMap)
+ assertEquals(1, newMap.get(1))
+ }
+
+ @Test
+ fun canCreateForward() {
+ val map = testMap(0 until 100)
+ assertNotNull(map)
+ for (i in 0 until 100) {
+ assertEquals(i, map.get(i.toLong()))
+ }
+ for (i in -100 until 0) {
+ assertNull(map.get(i.toLong()))
+ }
+ for (i in 100 until 200) {
+ assertNull(map.get(i.toLong()))
+ }
+ }
+
+ @Test
+ fun canCreateBackward() {
+ val map = testMap((0 until 100).reversed())
+ assertNotNull(map)
+ for (i in 0 until 100) {
+ assertEquals(i, map.get(i.toLong()))
+ }
+ for (i in -100 until 0) {
+ assertNull(map.get(i.toLong()))
+ }
+ for (i in 100 until 200) {
+ assertNull(map.get(i.toLong()))
+ }
+ }
+
+ @Test
+ fun canCreateRandom() {
+ val list = Array<Long>(100) { it.toLong() }
+ val rand = Random(1337)
+ list.shuffle(rand)
+ var map = emptyThreadMap()
+ for (item in list) {
+ map = map.newWith(item, item)
+ }
+ for (i in 0 until 100) {
+ assertEquals(i.toLong(), map.get(i.toLong()))
+ }
+ for (i in -100 until 0) {
+ assertNull(map.get(i.toLong()))
+ }
+ for (i in 100 until 200) {
+ assertNull(map.get(i.toLong()))
+ }
+ }
+
+ @Test
+ fun canRemoveOne() {
+ val map = testMap(1..10)
+ val set = map.trySet(5, null)
+ assertTrue(set)
+ for (i in 1..10) {
+ if (i == 5) {
+ assertNull(map.get(i.toLong()))
+ } else {
+ assertEquals(i, map.get(i.toLong()))
+ }
+ }
+ }
+
+ @Test
+ fun canRemoveOneThenAddOne() {
+ val map = testMap(1..10)
+ val set = map.trySet(5, null)
+ assertTrue(set)
+ val newMap = map.newWith(11, 11)
+ assertNull(newMap.get(5))
+ assertEquals(11, newMap.get(11))
+ }
+
+ private fun emptyThreadMap() = ThreadMap(0, LongArray(0), arrayOfNulls(0))
+
+ private fun testMap(intProgression: IntProgression): ThreadMap {
+ var result = emptyThreadMap()
+ for (i in intProgression) {
+ result = result.newWith(i.toLong(), i)
+ }
+ return result
+ }
+}
+
+/**
+ * Test the thread lcoal variable
+ */
+class SnapshotThreadLocalTests {
+ @Test
+ fun canCreate() {
+ val local = SnapshotThreadLocal<Int>()
+ assertNotNull(local)
+ }
+
+ @Test
+ fun initalValueIsNull() {
+ val local = SnapshotThreadLocal<Int>()
+ assertNull(local.get())
+ }
+
+ @Test
+ fun canSetAndGetTheValue() {
+ val local = SnapshotThreadLocal<Int>()
+ local.set(100)
+ assertEquals(100, local.get())
+ }
+}
\ No newline at end of file
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index 6c57480..feae637 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -42,9 +42,9 @@
api(project(":test-screenshot"))
implementation(KOTLIN_STDLIB_COMMON)
- implementation(project(":benchmark:benchmark-junit4"))
implementation(project(":compose:runtime:runtime"))
- implementation(project(":compose:ui:ui"))
+ implementation(project(":compose:ui:ui-unit"))
+ implementation(project(":compose:ui:ui-graphics"))
implementation(ANDROIDX_TEST_RULES)
// This has stub APIs for access to legacy Android APIs, so we don't want
@@ -68,9 +68,9 @@
sourceSets {
commonMain.dependencies {
implementation(KOTLIN_STDLIB_COMMON)
- implementation(project(":benchmark:benchmark-junit4"))
implementation(project(":compose:runtime:runtime"))
- implementation(project(":compose:ui:ui"))
+ implementation(project(":compose:ui:ui-unit"))
+ implementation(project(":compose:ui:ui-graphics"))
implementation(project(":compose:ui:ui-test-junit4"))
}
diff --git a/compose/test-utils/lint-baseline.xml b/compose/test-utils/lint-baseline.xml
index 988d5bd..0eddc06 100644
--- a/compose/test-utils/lint-baseline.xml
+++ b/compose/test-utils/lint-baseline.xml
@@ -1,26 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
+<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
<issue
id="BanSynchronizedMethods"
@@ -40,29 +19,7 @@
errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.android.kt"
- line="349"
- column="1"/>
- </issue>
-
- <issue
- id="BanTargetApiAnnotation"
- message="Uses @TargetApi annotation"
- errorLine1="@TargetApi(Build.VERSION_CODES.Q)"
- errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/testutils/benchmark/android/AndroidTestCaseRunner.android.kt"
- line="159"
- column="1"/>
- </issue>
-
- <issue
- id="BanTargetApiAnnotation"
- message="Uses @TargetApi annotation"
- errorLine1="@TargetApi(Build.VERSION_CODES.Q)"
- errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/testutils/benchmark/BenchmarkHelpers.android.kt"
- line="42"
+ line="344"
column="1"/>
</issue>
@@ -73,7 +30,7 @@
errorLine2=" ~~~~~~~~~~~~">
<location
file="src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.android.kt"
- line="278"
+ line="273"
column="29"/>
</issue>
@@ -84,7 +41,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~">
<location
file="src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.android.kt"
- line="351"
+ line="346"
column="30"/>
</issue>
@@ -95,118 +52,30 @@
errorLine2=" ~~~~~~~~~~~">
<location
file="src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.android.kt"
+ line="349"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="UnsafeNewApiCall"
+ message="This call is to a method from API 29, the call containing class androidx.compose.testutils.RenderNodeCapture 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 renderNode.beginRecording()"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.android.kt"
+ line="350"
+ column="27"/>
+ </issue>
+
+ <issue
+ id="UnsafeNewApiCall"
+ message="This call is to a method from API 29, the call containing class androidx.compose.testutils.RenderNodeCapture 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=" renderNode.endRecording()"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.android.kt"
line="354"
column="20"/>
</issue>
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 29, the call containing class androidx.compose.testutils.RenderNodeCapture 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 renderNode.beginRecording()"
- errorLine2=" ~~~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.android.kt"
- line="355"
- column="27"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 29, the call containing class androidx.compose.testutils.RenderNodeCapture 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=" renderNode.endRecording()"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/testutils/AndroidComposeTestCaseRunner.android.kt"
- line="359"
- column="20"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 29, the call containing class androidx.compose.testutils.benchmark.android.RenderNodeCapture 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=" private val renderNode = RenderNode("Test")"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/testutils/benchmark/android/AndroidTestCaseRunner.android.kt"
- line="161"
- column="30"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 29, the call containing class androidx.compose.testutils.benchmark.android.RenderNodeCapture 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=" renderNode.setPosition(0, 0, width, height)"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/testutils/benchmark/android/AndroidTestCaseRunner.android.kt"
- line="164"
- column="20"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 29, the call containing class androidx.compose.testutils.benchmark.android.RenderNodeCapture 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 renderNode.beginRecording()"
- errorLine2=" ~~~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/testutils/benchmark/android/AndroidTestCaseRunner.android.kt"
- line="165"
- column="27"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 29, the call containing class androidx.compose.testutils.benchmark.android.RenderNodeCapture 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=" renderNode.endRecording()"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/testutils/benchmark/android/AndroidTestCaseRunner.android.kt"
- line="169"
- column="20"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 29, the call containing class androidx.compose.testutils.benchmark.RenderNodeCapture 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=" private val renderNode = RenderNode("Test")"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/testutils/benchmark/BenchmarkHelpers.android.kt"
- line="44"
- column="30"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 29, the call containing class androidx.compose.testutils.benchmark.RenderNodeCapture 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=" renderNode.setPosition(0, 0, width, height)"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/testutils/benchmark/BenchmarkHelpers.android.kt"
- line="47"
- column="20"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 29, the call containing class androidx.compose.testutils.benchmark.RenderNodeCapture 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 Canvas(renderNode.beginRecording())"
- errorLine2=" ~~~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/testutils/benchmark/BenchmarkHelpers.android.kt"
- line="48"
- column="34"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 29, the call containing class androidx.compose.testutils.benchmark.RenderNodeCapture 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=" renderNode.endRecording()"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/testutils/benchmark/BenchmarkHelpers.android.kt"
- line="52"
- column="20"/>
- </issue>
-
</issues>
diff --git a/compose/ui/ui-android-stubs/lint-baseline.xml b/compose/ui/ui-android-stubs/lint-baseline.xml
deleted file mode 100644
index 8f1aa4b..0000000
--- a/compose/ui/ui-android-stubs/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/compose/ui/ui-geometry/lint-baseline.xml b/compose/ui/ui-geometry/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/ui/ui-geometry/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/ui/ui-graphics/benchmark/build.gradle b/compose/ui/ui-graphics/benchmark/build.gradle
new file mode 100644
index 0000000..dcb734b
--- /dev/null
+++ b/compose/ui/ui-graphics/benchmark/build.gradle
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXUiPlugin")
+ id("org.jetbrains.kotlin.android")
+ id("androidx.benchmark")
+}
+
+dependencies {
+ kotlinPlugin project(":compose:compiler:compiler")
+
+ implementation project(":compose:foundation:foundation")
+ implementation project(":compose:runtime:runtime")
+ implementation project(":compose:benchmark-utils")
+ implementation project(":compose:ui:ui")
+ implementation(KOTLIN_STDLIB)
+
+ androidTestImplementation project(":benchmark:benchmark-junit4")
+ androidTestImplementation project(":benchmark:benchmark-macro-junit4")
+ androidTestImplementation(ANDROIDX_TEST_RULES)
+}
diff --git a/compose/ui/ui-graphics/benchmark/src/androidTest/AndroidManifest.xml b/compose/ui/ui-graphics/benchmark/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..b5d6704
--- /dev/null
+++ b/compose/ui/ui-graphics/benchmark/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="androidx.compose.ui.graphics.benchmark">
+
+ <!--
+ ~ Important: disable debuggable for accurate performance results
+ -->
+ <application
+ android:debuggable="false"
+ tools:replace="android:debuggable">
+ <!-- enable profileableByShell for non-intrusive profiling tools -->
+ <!--suppress AndroidElementNotAllowed -->
+ <profileable android:shell="true"/>
+ </application>
+</manifest>
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/VectorBenchmark.kt b/compose/ui/ui-graphics/benchmark/src/androidTest/java/androidx/compose/ui/graphics/benchmark/VectorBenchmark.kt
similarity index 92%
rename from compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/VectorBenchmark.kt
rename to compose/ui/ui-graphics/benchmark/src/androidTest/java/androidx/compose/ui/graphics/benchmark/VectorBenchmark.kt
index fb531d0..0938440 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/VectorBenchmark.kt
+++ b/compose/ui/ui-graphics/benchmark/src/androidTest/java/androidx/compose/ui/graphics/benchmark/VectorBenchmark.kt
@@ -14,20 +14,18 @@
* limitations under the License.
*/
-package androidx.ui.benchmark.test
+package androidx.compose.ui.graphics.benchmark
import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
import androidx.compose.testutils.benchmark.benchmarkFirstCompose
import androidx.compose.testutils.benchmark.benchmarkFirstDraw
import androidx.compose.testutils.benchmark.benchmarkFirstLayout
import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
-import androidx.ui.integration.test.framework.ProgrammaticVectorTestCase
-import androidx.ui.integration.test.framework.XmlVectorTestCase
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import androidx.test.ext.junit.runners.AndroidJUnit4
/**
* Benchmark to compare performance of [parsing a vector asset from XML][XmlVectorTestCase] and
@@ -78,4 +76,4 @@
fun programmatic_draw() {
benchmarkRule.benchmarkFirstDraw { ProgrammaticVectorTestCase() }
}
-}
+}
\ No newline at end of file
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/VectorBenchmarkWithTracing.kt b/compose/ui/ui-graphics/benchmark/src/androidTest/java/androidx/compose/ui/graphics/benchmark/VectorBenchmarkWithTracing.kt
similarity index 95%
rename from compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/VectorBenchmarkWithTracing.kt
rename to compose/ui/ui-graphics/benchmark/src/androidTest/java/androidx/compose/ui/graphics/benchmark/VectorBenchmarkWithTracing.kt
index aed3bfc..d4e4318 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/VectorBenchmarkWithTracing.kt
+++ b/compose/ui/ui-graphics/benchmark/src/androidTest/java/androidx/compose/ui/graphics/benchmark/VectorBenchmarkWithTracing.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.ui.benchmark.test
+package androidx.compose.ui.graphics.benchmark
import androidx.benchmark.macro.junit4.PerfettoRule
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -33,4 +33,4 @@
class VectorBenchmarkWithTracing : VectorBenchmark() {
@get:Rule
val perfettoRule = PerfettoRule()
-}
+}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/benchmark/src/main/AndroidManifest.xml b/compose/ui/ui-graphics/benchmark/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c628c53
--- /dev/null
+++ b/compose/ui/ui-graphics/benchmark/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.compose.ui.graphics.benchmark">
+ <application/>
+</manifest>
diff --git a/compose/integration-tests/src/main/java/androidx/ui/integration/test/framework/ImageVectorTestCase.kt b/compose/ui/ui-graphics/benchmark/src/main/java/androidx/compose/ui/graphics/benchmark/ImageVectorTestCase.kt
similarity index 96%
rename from compose/integration-tests/src/main/java/androidx/ui/integration/test/framework/ImageVectorTestCase.kt
rename to compose/ui/ui-graphics/benchmark/src/main/java/androidx/compose/ui/graphics/benchmark/ImageVectorTestCase.kt
index b2240f1..e7e5175 100644
--- a/compose/integration-tests/src/main/java/androidx/ui/integration/test/framework/ImageVectorTestCase.kt
+++ b/compose/ui/ui-graphics/benchmark/src/main/java/androidx/compose/ui/graphics/benchmark/ImageVectorTestCase.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.ui.integration.test.framework
+package androidx.compose.ui.graphics.benchmark
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -66,7 +66,7 @@
// TODO: should switch to async loading here, and force that to be run synchronously
@Composable
override fun getPainter() = painterResource(
- androidx.ui.integration.test.R.drawable.ic_baseline_menu_24
+ androidx.compose.ui.graphics.benchmark.R.drawable.ic_baseline_menu_24
)
override val testTag = "Xml"
diff --git a/compose/integration-tests/src/main/res/drawable/ic_baseline_menu_24.xml b/compose/ui/ui-graphics/benchmark/src/main/res/drawable/ic_baseline_menu_24.xml
similarity index 100%
rename from compose/integration-tests/src/main/res/drawable/ic_baseline_menu_24.xml
rename to compose/ui/ui-graphics/benchmark/src/main/res/drawable/ic_baseline_menu_24.xml
diff --git a/compose/integration-tests/src/main/res/drawable/ic_pathfill_sample.xml b/compose/ui/ui-graphics/benchmark/src/main/res/drawable/ic_pathfill_sample.xml
similarity index 100%
rename from compose/integration-tests/src/main/res/drawable/ic_pathfill_sample.xml
rename to compose/ui/ui-graphics/benchmark/src/main/res/drawable/ic_pathfill_sample.xml
diff --git a/compose/ui/ui-graphics/benchmark/test/build.gradle b/compose/ui/ui-graphics/benchmark/test/build.gradle
new file mode 100644
index 0000000..0a40f0d
--- /dev/null
+++ b/compose/ui/ui-graphics/benchmark/test/build.gradle
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXUiPlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ kotlinPlugin project(":compose:compiler:compiler")
+
+ androidTestImplementation project(":compose:foundation:foundation")
+ androidTestImplementation project(":compose:runtime:runtime")
+ androidTestImplementation project(":compose:test-utils")
+ androidTestImplementation project(":compose:ui:ui")
+ androidTestImplementation project(":compose:ui:ui-graphics")
+ androidTestImplementation project(":compose:ui:ui-graphics:ui-graphics-benchmark")
+ androidTestImplementation(KOTLIN_STDLIB)
+ androidTestImplementation(ANDROIDX_TEST_RULES)
+ androidTestImplementation(TRUTH)
+}
diff --git a/compose/ui/ui-graphics/benchmark/test/src/androidTest/AndroidManifest.xml b/compose/ui/ui-graphics/benchmark/test/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..11e4107
--- /dev/null
+++ b/compose/ui/ui-graphics/benchmark/test/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="androidx.compose.ui.graphics.benchmark.test">
+</manifest>
diff --git a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/ImageVectorTest.kt b/compose/ui/ui-graphics/benchmark/test/src/androidTest/java/androidx/compose/ui/graphics/benchmark/test/ImageVectorTest.kt
similarity index 90%
rename from compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/ImageVectorTest.kt
rename to compose/ui/ui-graphics/benchmark/test/src/androidTest/java/androidx/compose/ui/graphics/benchmark/test/ImageVectorTest.kt
index 3b9e99e..b052ce0 100644
--- a/compose/integration-tests/src/androidTest/java/androidx/ui/integration/test/ImageVectorTest.kt
+++ b/compose/ui/ui-graphics/benchmark/test/src/androidTest/java/androidx/compose/ui/graphics/benchmark/test/ImageVectorTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.ui.integration.test
+package androidx.compose.ui.graphics.benchmark.test
import android.os.Build
import androidx.compose.foundation.Image
@@ -22,6 +22,8 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.graphics.benchmark.ProgrammaticVectorTestCase
+import androidx.compose.ui.graphics.benchmark.XmlVectorTestCase
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
@@ -32,9 +34,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
-import androidx.ui.integration.test.framework.ProgrammaticVectorTestCase
-import androidx.ui.integration.test.framework.XmlVectorTestCase
-import org.junit.Assert.assertArrayEquals
+import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
@@ -83,7 +83,7 @@
}
}
- assertArrayEquals(xmlPixelArray, programmaticBitmapArray)
+ Assert.assertArrayEquals(xmlPixelArray, programmaticBitmapArray)
}
@Test
@@ -94,7 +94,9 @@
with(LocalDensity.current) {
insetRectSize = (10f * this.density).roundToInt()
}
- val imageVector = painterResource(R.drawable.ic_pathfill_sample)
+ val imageVector = painterResource(
+ androidx.compose.ui.graphics.benchmark.R.drawable.ic_pathfill_sample
+ )
Image(imageVector, null, modifier = Modifier.testTag(testTag))
}
@@ -131,4 +133,4 @@
)
}
}
-}
+}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/benchmark/test/src/main/AndroidManifest.xml b/compose/ui/ui-graphics/benchmark/test/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a4a839c
--- /dev/null
+++ b/compose/ui/ui-graphics/benchmark/test/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.compose.ui.graphics.benchmark.test">
+ <application/>
+</manifest>
diff --git a/compose/ui/ui-graphics/lint-baseline.xml b/compose/ui/ui-graphics/lint-baseline.xml
index 6ad5a4a..f5995f5 100644
--- a/compose/ui/ui-graphics/lint-baseline.xml
+++ b/compose/ui/ui-graphics/lint-baseline.xml
@@ -1,37 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 29, the call containing class androidx.compose.ui.graphics.AndroidPaint_androidKt 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=" this.blendMode = mode.toAndroidBlendMode()"
- errorLine2=" ~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPaint.android.kt"
- line="134"
- column="14"/>
- </issue>
+<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
<issue
id="UnsafeNewApiCall"
diff --git a/compose/ui/ui-graphics/samples/lint-baseline.xml b/compose/ui/ui-graphics/samples/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/ui/ui-graphics/samples/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/ui/ui-inspection/build.gradle b/compose/ui/ui-inspection/build.gradle
index 63e1254..4d5dab2 100644
--- a/compose/ui/ui-inspection/build.gradle
+++ b/compose/ui/ui-inspection/build.gradle
@@ -35,7 +35,7 @@
// because compose:ui-inspector can be run only in app with compose:ui:ui
// thus all its transitive dependencies will be present too.
compileOnly(KOTLIN_STDLIB)
- compileOnly(projectOrArtifact(":inspection:inspection"))
+ compileOnly("androidx.inspection:inspection:1.0.0")
compileOnly(project(":compose:runtime:runtime"))
compileOnly(project(":compose:ui:ui"))
// we ignore its transitive dependencies, because ui-inspection should
@@ -101,4 +101,4 @@
inspection {
name = "compose-ui-inspection.jar"
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui-inspection/lint-baseline.xml b/compose/ui/ui-inspection/lint-baseline.xml
deleted file mode 100644
index 8f1aa4b..0000000
--- a/compose/ui/ui-inspection/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/ParametersTest.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/ParametersTest.kt
index 1fb1391..3a3b4ce 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/ParametersTest.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/ParametersTest.kt
@@ -20,6 +20,7 @@
import androidx.compose.ui.inspection.rules.sendCommand
import androidx.compose.ui.inspection.testdata.ParametersTestActivity
import androidx.compose.ui.inspection.util.GetComposablesCommand
+import androidx.compose.ui.inspection.util.GetParameterDetailsCommand
import androidx.compose.ui.inspection.util.GetParametersCommand
import androidx.compose.ui.inspection.util.toMap
import androidx.test.filters.LargeTest
@@ -29,6 +30,7 @@
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetComposablesResponse
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetParametersResponse
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Parameter
+import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.StringEntry
import org.junit.Rule
import org.junit.Test
@@ -65,8 +67,8 @@
val lambdaValue = params.find("onClick")!!.lambdaValue
assertThat(lambdaValue.fileName.resolve(params)).isEqualTo("ParametersTestActivity.kt")
- assertThat(lambdaValue.startLineNumber).isEqualTo(47)
- assertThat(lambdaValue.endLineNumber).isEqualTo(47)
+ assertThat(lambdaValue.startLineNumber).isEqualTo(48)
+ assertThat(lambdaValue.endLineNumber).isEqualTo(48)
assertThat(lambdaValue.packageName.resolve(params))
.isEqualTo("androidx.compose.ui.inspection.testdata")
}
@@ -83,12 +85,47 @@
val lambdaValue = params.find("onClick")!!.lambdaValue
assertThat(lambdaValue.fileName.resolve(params)).isEqualTo("ParametersTestActivity.kt")
- assertThat(lambdaValue.startLineNumber).isEqualTo(50)
- assertThat(lambdaValue.endLineNumber).isEqualTo(50)
+ assertThat(lambdaValue.startLineNumber).isEqualTo(51)
+ assertThat(lambdaValue.endLineNumber).isEqualTo(51)
assertThat(lambdaValue.functionName.resolve(params)).isEqualTo("testClickHandler")
assertThat(lambdaValue.packageName.resolve(params))
.isEqualTo("androidx.compose.ui.inspection.testdata")
}
+
+ @Test
+ fun intArray(): Unit = runBlocking {
+ val tester = rule.inspectorTester
+ val nodes = tester.sendCommand(GetComposablesCommand(rule.rootId)).getComposablesResponse
+
+ val function = nodes.filter("FunctionWithIntArray").single()
+ val params = tester.sendCommand(GetParametersCommand(rule.rootId, function.id))
+ .getParametersResponse
+
+ val intArray = params.find("intArray")!!
+ var strings = params.stringsList
+ assertThat(intArray.elementsCount).isEqualTo(5)
+ checkParam(strings, intArray.elementsList[0], "[0]", 10)
+ checkParam(strings, intArray.elementsList[1], "[1]", 11)
+ checkParam(strings, intArray.elementsList[2], "[2]", 12)
+ checkParam(strings, intArray.elementsList[3], "[3]", 13)
+ checkParam(strings, intArray.elementsList[4], "[4]", 14)
+
+ val expanded =
+ tester.sendCommand(
+ GetParameterDetailsCommand(
+ rule.rootId,
+ intArray.reference,
+ startIndex = 5,
+ maxElements = 5
+ )
+ ).getParameterDetailsResponse
+ val intArray2 = expanded.parameter
+ strings = expanded.stringsList
+ assertThat(intArray2.elementsCount).isEqualTo(3)
+ checkParam(strings, intArray2.elementsList[0], "[5]", 15)
+ checkParam(strings, intArray2.elementsList[1], "[6]", 16)
+ checkParam(strings, intArray2.elementsList[2], "[7]", 17)
+ }
}
private fun Int.resolve(response: GetParametersResponse): String? {
@@ -110,4 +147,16 @@
}
private fun ComposableNode.flatten(): List<ComposableNode> =
- listOf(this).plus(this.childrenList.flatMap { it.flatten() })
\ No newline at end of file
+ listOf(this).plus(this.childrenList.flatMap { it.flatten() })
+
+private fun checkParam(
+ stringList: List<StringEntry>,
+ param: Parameter,
+ name: String,
+ value: Int,
+ index: Int = -1
+) {
+ assertThat(stringList.toMap()[param.name]).isEqualTo(name)
+ assertThat(param.int32Value).isEqualTo(value)
+ assertThat(param.index).isEqualTo(index)
+}
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/UnknownResponseTest.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/UnknownResponseTest.kt
new file mode 100644
index 0000000..d4df199
--- /dev/null
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/UnknownResponseTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.inspection
+
+import androidx.compose.ui.inspection.rules.ComposeInspectionRule
+import androidx.compose.ui.inspection.testdata.TestActivity
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Command
+import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Response
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+class UnknownResponseTest {
+ @get:Rule
+ val rule = ComposeInspectionRule(TestActivity::class)
+
+ @Test
+ fun invalidBytesReturnedAsUnknownResponse(): Unit = runBlocking {
+ val invalidBytes = (1..99).map { it.toByte() }.toByteArray()
+ val responseBytes = rule.inspectorTester.sendCommand(invalidBytes)
+ val response = Response.parseFrom(responseBytes)
+
+ assertThat(response.specializedCase)
+ .isEqualTo(Response.SpecializedCase.UNKNOWN_COMMAND_RESPONSE)
+ assertThat(response.unknownCommandResponse.commandBytes.toByteArray())
+ .isEqualTo(invalidBytes)
+ }
+
+ @Test
+ fun unhandledCommandCaseReturnedAsUnknownResponse(): Unit = runBlocking {
+ val invalidCommand = Command.getDefaultInstance()
+ // This invalid case is handled by an else branch in ComposeLayoutInspector. In practice,
+ // this could also happen when a newer version of Studio sends a new command to an older
+ // version of an inspector.
+ assertThat(invalidCommand.specializedCase)
+ .isEqualTo(Command.SpecializedCase.SPECIALIZED_NOT_SET)
+
+ val commandBytes = invalidCommand.toByteArray()
+ val responseBytes = rule.inspectorTester.sendCommand(commandBytes)
+ val response = Response.parseFrom(responseBytes)
+
+ assertThat(response.specializedCase)
+ .isEqualTo(Response.SpecializedCase.UNKNOWN_COMMAND_RESPONSE)
+ assertThat(response.unknownCommandResponse.commandBytes.toByteArray())
+ .isEqualTo(commandBytes)
+ }
+}
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt
index 6504b6d..c98b9a9 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTreeTest.kt
@@ -71,6 +71,7 @@
import kotlin.math.roundToInt
private const val DEBUG = false
+private const val ROOT_ID = 3L
@LargeTest
@RunWith(AndroidJUnit4::class)
@@ -456,14 +457,10 @@
}
if (checkParameters) {
- val params = builder.convertParameters(node)
+ val params = builder.convertParameters(ROOT_ID, node)
val receiver = ParameterValidationReceiver(params.listIterator())
receiver.block()
- if (receiver.parameterIterator.hasNext()) {
- val elementNames = mutableListOf<String>()
- receiver.parameterIterator.forEachRemaining { elementNames.add(it.name) }
- error("$name: has more parameters like: ${elementNames.joinToString()}")
- }
+ receiver.checkFinished(name)
}
}
}
@@ -526,7 +523,7 @@
println()
print(")")
if (generateParameters && node.parameters.isNotEmpty()) {
- generateParameters(builder.convertParameters(node), 0)
+ generateParameters(builder.convertParameters(ROOT_ID, node), 0)
}
println()
node.children.forEach { generateValidate(it, builder) }
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/ParameterFactoryTest.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/ParameterFactoryTest.kt
index 57f9325..4fc2af8 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/ParameterFactoryTest.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/ParameterFactoryTest.kt
@@ -80,6 +80,10 @@
import org.junit.Test
import org.junit.runner.RunWith
+private const val ROOT_ID = 3L
+private const val NODE_ID = -7L
+private const val PARAM_INDEX = 4
+
@Suppress("unused")
private fun topLevelFunction() {
}
@@ -88,9 +92,12 @@
@RunWith(AndroidJUnit4::class)
class ParameterFactoryTest {
private val factory = ParameterFactory(InlineClassConverter())
+ private val originalMaxRecursions = factory.maxRecursions
+ private val originalMaxIterable = factory.maxIterable
private val node = MutableInspectorNode().apply {
width = 1000
height = 500
+ id = NODE_ID
}.build()
@Before
@@ -101,6 +108,8 @@
@After
fun after() {
+ factory.maxRecursions = originalMaxRecursions
+ factory.maxIterable = originalMaxIterable
isDebugInspectorInfoEnabled = false
}
@@ -186,7 +195,7 @@
@Test
fun testBorder() {
- validate(factory.create(node, "borderstroke", BorderStroke(2.0.dp, Color.Magenta))!!) {
+ validate(create("borderstroke", BorderStroke(2.0.dp, Color.Magenta))) {
parameter("borderstroke", ParameterType.String, "BorderStroke") {
parameter("brush", ParameterType.Color, Color.Magenta.toArgb())
parameter("width", ParameterType.DimensionDp, 2.0f)
@@ -199,24 +208,23 @@
assertThat(lookup(SolidColor(Color.Red)))
.isEqualTo(ParameterType.Color to Color.Red.toArgb())
validate(
- factory.create(
- node,
+ create(
"brush",
Brush.linearGradient(
colors = listOf(Color.Red, Color.Blue),
start = Offset(0.0f, 0.5f),
end = Offset(5.0f, 10.0f)
)
- )!!
+ )
) {
parameter("brush", ParameterType.String, "LinearGradient") {
- parameter("colors", ParameterType.String, "") {
- parameter("0", ParameterType.Color, Color.Red.toArgb())
- parameter("1", ParameterType.Color, Color.Blue.toArgb())
+ parameter("colors", ParameterType.Iterable, "") {
+ parameter("[0]", ParameterType.Color, Color.Red.toArgb())
+ parameter("[1]", ParameterType.Color, Color.Blue.toArgb())
}
// Parameters are traversed in alphabetical order through reflection queries.
// Validate createdSize exists before validating end parameter
- parameter("createdSize", ParameterType.String, "Unspecified")
+ parameter("createdSize", ParameterType.String, "Unspecified", index = 5)
parameter("end", ParameterType.String, Offset::class.java.simpleName) {
parameter("x", ParameterType.DimensionDp, 2.5f)
parameter("y", ParameterType.DimensionDp, 5.0f)
@@ -225,7 +233,7 @@
parameter("x", ParameterType.DimensionDp, 0.0f)
parameter("y", ParameterType.DimensionDp, 0.25f)
}
- parameter("tileMode", ParameterType.String, "Clamp")
+ parameter("tileMode", ParameterType.String, "Clamp", index = 4)
}
}
// TODO: add tests for RadialGradient & ShaderBrush
@@ -243,8 +251,9 @@
fun testComposableLambda() = runBlocking {
// capture here to force the lambda to not be created as a singleton.
val capture = "Hello World"
+ @Suppress("COMPOSABLE_INVOCATION")
val c: @Composable () -> Unit = { Text(text = capture) }
- val result = lookup(c as Any) ?: error("Lookup of ComposableLambda failed")
+ val result = lookup(c as Any)
val array = result.second as Array<*>
assertThat(result.first).isEqualTo(ParameterType.Lambda)
assertThat(array).hasLength(1)
@@ -256,12 +265,7 @@
@Ignore
@Test
fun testCornerBasedShape() {
- validate(
- factory.create(
- node, "corner",
- RoundedCornerShape(2.0.dp, 0.5.dp, 2.5.dp, 0.7.dp)
- )!!
- ) {
+ validate(create("corner", RoundedCornerShape(2.0.dp, 0.5.dp, 2.5.dp, 0.7.dp))) {
parameter("corner", ParameterType.String, RoundedCornerShape::class.java.simpleName) {
parameter("bottomEnd", ParameterType.DimensionDp, 2.5f)
parameter("bottomStart", ParameterType.DimensionDp, 0.7f)
@@ -269,7 +273,7 @@
parameter("topStart", ParameterType.DimensionDp, 2.0f)
}
}
- validate(factory.create(node, "corner", CutCornerShape(2))!!) {
+ validate(create("corner", CutCornerShape(2))) {
parameter("corner", ParameterType.String, CutCornerShape::class.java.simpleName) {
parameter("bottomEnd", ParameterType.DimensionDp, 5.0f)
parameter("bottomStart", ParameterType.DimensionDp, 5.0f)
@@ -277,7 +281,7 @@
parameter("topStart", ParameterType.DimensionDp, 5.0f)
}
}
- validate(factory.create(node, "corner", RoundedCornerShape(1.0f, 10.0f, 2.0f, 3.5f))!!) {
+ validate(create("corner", RoundedCornerShape(1.0f, 10.0f, 2.0f, 3.5f))) {
parameter("corner", ParameterType.String, RoundedCornerShape::class.java.simpleName) {
parameter("bottomEnd", ParameterType.DimensionDp, 1.0f)
parameter("bottomStart", ParameterType.DimensionDp, 1.75f)
@@ -369,12 +373,12 @@
@Test
fun testFunctionReference() {
val ref1 = ::testInt
- val map1 = lookup(ref1)!!
+ val map1 = lookup(ref1)
val array1 = map1.second as Array<*>
assertThat(map1.first).isEqualTo(ParameterType.FunctionReference)
assertThat(array1.contentEquals(arrayOf(ref1, "testInt"))).isTrue()
val ref2 = ::topLevelFunction
- val map2 = lookup(ref2)!!
+ val map2 = lookup(ref2)
val array2 = map2.second as Array<*>
assertThat(map2.first).isEqualTo(ParameterType.FunctionReference)
assertThat(array2.contentEquals(arrayOf(ref2, "topLevelFunction"))).isTrue()
@@ -382,7 +386,7 @@
@Test
fun testPaddingValues() {
- validate(factory.create(node, "padding", PaddingValues(2.0.dp, 0.5.dp, 2.5.dp, 0.7.dp))!!) {
+ validate(create("padding", PaddingValues(2.0.dp, 0.5.dp, 2.5.dp, 0.7.dp))) {
parameter(
"padding",
ParameterType.String,
@@ -404,7 +408,7 @@
@Test
fun testLambda() {
val a: (Int) -> Int = { it }
- val map = lookup(a)!!
+ val map = lookup(a)
val array = map.second as Array<*>
assertThat(map.first).isEqualTo(ParameterType.Lambda)
assertThat(array.contentEquals(arrayOf<Any>(a))).isTrue()
@@ -418,10 +422,10 @@
@Test
fun testLocaleList() {
- validate(factory.create(node, "locales", LocaleList(Locale("fr-ca"), Locale("fr-be")))!!) {
- parameter("locales", ParameterType.String, "") {
- parameter("0", ParameterType.String, "fr-CA")
- parameter("1", ParameterType.String, "fr-BE")
+ validate(create("locales", LocaleList(Locale("fr-ca"), Locale("fr-be")))) {
+ parameter("locales", ParameterType.Iterable, "") {
+ parameter("[0]", ParameterType.String, "fr-CA")
+ parameter("[1]", ParameterType.String, "fr-BE")
}
}
}
@@ -432,10 +436,80 @@
}
@Test
+ fun testShortIntArray() {
+ factory.maxIterable = 10
+ val value = intArrayOf(10, 11, 12)
+ val parameter = create("array", value)
+ validate(parameter) {
+ parameter("array", ParameterType.Iterable, "") {
+ parameter("[0]", ParameterType.Int32, 10)
+ parameter("[1]", ParameterType.Int32, 11)
+ parameter("[2]", ParameterType.Int32, 12)
+ }
+ }
+ }
+
+ @Test
+ fun testLongIntArray() {
+ factory.maxIterable = 5
+ val value = intArrayOf(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23)
+ val refToSelf = ref()
+ val parameter = create("array", value)
+ validate(parameter) {
+ parameter("array", ParameterType.Iterable, "", refToSelf) {
+ parameter("[0]", ParameterType.Int32, 10)
+ parameter("[1]", ParameterType.Int32, 11)
+ parameter("[2]", ParameterType.Int32, 12)
+ parameter("[3]", ParameterType.Int32, 13)
+ parameter("[4]", ParameterType.Int32, 14)
+ }
+ }
+
+ // If we need to retrieve more array elements we call "factory.expand" with the reference:
+ validate(factory.expand(ROOT_ID, node, "array", value, refToSelf, 5, 5)!!) {
+ parameter("array", ParameterType.Iterable, "", refToSelf) {
+ parameter("[5]", ParameterType.Int32, 15)
+ parameter("[6]", ParameterType.Int32, 16)
+ parameter("[7]", ParameterType.Int32, 17)
+ parameter("[8]", ParameterType.Int32, 18)
+ parameter("[9]", ParameterType.Int32, 19)
+ }
+ }
+
+ // Call "factory.expand" again to retrieve more:
+ validate(factory.expand(ROOT_ID, node, "array", value, refToSelf, 10, 5)!!) {
+ // This time we reached the end of the array, and we do not get a reference to get more
+ parameter("array", ParameterType.Iterable, "") {
+ parameter("[10]", ParameterType.Int32, 20)
+ parameter("[11]", ParameterType.Int32, 21)
+ parameter("[12]", ParameterType.Int32, 22)
+ parameter("[13]", ParameterType.Int32, 23)
+ }
+ }
+ }
+
+ @Test
+ fun testListWithNullElement() {
+ factory.maxIterable = 3
+ val value = listOf("Hello", null, "World")
+ val parameter = create("array", value)
+ validate(parameter) {
+ // Here we get all the available elements from the list.
+ // There is no need to go back for more data, and the iterable does not have a
+ // reference for doing so.
+ parameter("array", ParameterType.Iterable, "") {
+ parameter("[0]", ParameterType.String, "Hello")
+ parameter("[2]", ParameterType.String, "World", index = 2)
+ }
+ }
+ }
+
+ @Test
fun testModifier() {
+ factory.maxRecursions = 4
validate(
- factory.create(
- node, "modifier",
+ create(
+ "modifier",
Modifier
.background(Color.Blue)
.border(width = 5.dp, color = Color.Red)
@@ -444,7 +518,7 @@
.wrapContentHeight(Alignment.Bottom)
.width(30.0.dp)
.paint(TestPainter(10f, 20f))
- )!!
+ )
) {
parameter("modifier", ParameterType.String, "") {
parameter("background", ParameterType.Color, Color.Blue.toArgb()) {
@@ -472,7 +546,7 @@
parameter("painter", ParameterType.String, "TestPainter") {
parameter("alpha", ParameterType.Float, 1.0f)
parameter("color", ParameterType.Color, Color.Red.toArgb())
- parameter("drawLambda", ParameterType.Lambda, null)
+ parameter("drawLambda", ParameterType.Lambda, null, index = 6)
parameter("height", ParameterType.Float, 20.0f)
parameter("intrinsicSize", ParameterType.String, "Size") {
parameter("height", ParameterType.Float, 20.0f)
@@ -481,8 +555,8 @@
parameter("packedValue", ParameterType.Int64, 4692750812821061632L)
parameter("width", ParameterType.Float, 10.0f)
}
- parameter("layoutDirection", ParameterType.String, "Ltr")
- parameter("useLayer", ParameterType.Boolean, false)
+ parameter("layoutDirection", ParameterType.String, "Ltr", index = 8)
+ parameter("useLayer", ParameterType.Boolean, false, index = 9)
parameter("width", ParameterType.Float, 10.0f)
}
parameter("sizeToIntrinsics", ParameterType.Boolean, true)
@@ -493,7 +567,7 @@
@Test
fun testSingleModifier() {
- validate(factory.create(node, "modifier", Modifier.padding(2.0.dp))!!) {
+ validate(create("modifier", Modifier.padding(2.0.dp))) {
parameter("modifier", ParameterType.String, "") {
parameter("padding", ParameterType.DimensionDp, 2.0f)
}
@@ -502,7 +576,7 @@
@Test
fun testSingleModifierWithParameters() {
- validate(factory.create(node, "modifier", Modifier.padding(1.dp, 2.dp, 3.dp, 4.dp))!!) {
+ validate(create("modifier", Modifier.padding(1.dp, 2.dp, 3.dp, 4.dp))) {
parameter("modifier", ParameterType.String, "") {
parameter("padding", ParameterType.String, "") {
parameter("bottom", ParameterType.DimensionDp, 4.0f)
@@ -516,66 +590,124 @@
@Test
fun testOffset() {
- validate(factory.create(node, "offset", Offset(1.0f, 5.0f))!!) {
+ validate(create("offset", Offset(1.0f, 5.0f))) {
parameter("offset", ParameterType.String, Offset::class.java.simpleName) {
parameter("x", ParameterType.DimensionDp, 0.5f)
parameter("y", ParameterType.DimensionDp, 2.5f)
}
}
- validate(factory.create(node, "offset", Offset.Zero)!!) {
+ validate(create("offset", Offset.Zero)) {
parameter("offset", ParameterType.String, "Zero")
}
}
@Test
fun testRecursiveStructure() {
- val v1 = MyClass()
- val v2 = MyClass()
+ val v1 = MyClass("v1")
+ val v2 = MyClass("v2")
v1.other = v2
v2.other = v1
+ v1.self = v1
+ v2.self = v2
val name = MyClass::class.java.simpleName
- validate(factory.create(node, "mine", v1)!!) {
+ validate(create("mine", v1)) {
parameter("mine", ParameterType.String, name) {
+ parameter("name", ParameterType.String, "v1")
parameter("other", ParameterType.String, name) {
- parameter("other", ParameterType.String, name) {
- parameter("other", ParameterType.String, name) {
- parameter("other", ParameterType.String, name) {
- parameter("other", ParameterType.String, name) {
- parameter("other", ParameterType.String, name) {
- parameter("other", ParameterType.String, name) {
- parameter("other", ParameterType.String, name) {
- parameter("other", ParameterType.String, name)
- }
- }
- }
- }
- }
- }
- }
+ parameter("name", ParameterType.String, "v2")
+ // v2.other is expected to reference v1 which is already found
+ parameter("other", ParameterType.String, name, ref())
+
+ // v2.self is expected to reference v2 which is already found
+ parameter("self", ParameterType.String, name, ref(1))
}
+ // v1.self is expected to reference v1 which is already found
+ parameter("self", ParameterType.String, name, ref())
}
}
}
@Test
- fun testDoNotRecurseIntoAndroidAndJavaPackages() {
+ fun testMissingChildParameters() {
+ val v1 = MyClass("v1")
+ val v2 = MyClass("v2")
+ val v3 = MyClass("v3")
+ val v4 = MyClass("v4")
+ val v5 = MyClass("v5")
+ v1.self = v1
+ v1.third = v2
+ v2.other = v3
+ v2.third = v1
+ v3.other = v4
+ v4.other = v5
+ val name = MyClass::class.java.simpleName
+
+ // Limit the recursions for this test to validate parameter nodes with missing children.
+ factory.maxRecursions = 2
+
+ val parameter = create("v1", v1)
+ val v2ref = ref(3, 1)
+ validate(parameter) {
+ parameter("v1", ParameterType.String, name) {
+ parameter("name", ParameterType.String, "v1")
+ parameter("self", ParameterType.String, name, ref(), index = 2)
+ parameter("third", ParameterType.String, name, index = 3) {
+ parameter("name", ParameterType.String, "v2")
+
+ // Expect the child elements for v2 to be missing from the parameter tree,
+ // which is indicated by the reference field being included for "other" here:
+ parameter("other", ParameterType.String, name, v2ref)
+ parameter("third", ParameterType.String, name, ref(), index = 3)
+ }
+ }
+ }
+
+ // If we need to retrieve the missing child nodes for v2 from above, we must
+ // call "factory.expand" with the reference:
+ val v4ref = ref(3, 1, 1, 1)
+ validate(factory.expand(ROOT_ID, node, "v1", v1, v2ref)!!) {
+ parameter("other", ParameterType.String, name) {
+ parameter("name", ParameterType.String, "v3")
+ parameter("other", ParameterType.String, name) {
+ parameter("name", ParameterType.String, "v4")
+
+ // Expect the child elements for v4 to be missing from the parameter tree,
+ // which is indicated by the reference field being included for "other" here:
+ parameter("other", ParameterType.String, name, v4ref)
+ }
+ }
+ }
+
+ // If we need to retrieve the missing child nodes for v4 from above, we must
+ // call "factory.expand" with the reference:
+ validate(factory.expand(ROOT_ID, node, "v1", v1, v4ref)!!) {
+ parameter("other", ParameterType.String, name) {
+ parameter("name", ParameterType.String, "v5")
+ }
+ }
+ }
+
+ @Test
+ fun testDoNotRecurseInto() {
runBlocking {
- assertThat(factory.create(node, "v1", java.net.URL("http://domain.com"))).isNull()
- assertThat(factory.create(node, "v1", android.app.Notification())).isNull()
+ assertThat(lookup(java.net.URL("http://domain.com")))
+ .isEqualTo(ParameterType.String to "")
+ assertThat(lookup(android.app.Notification()))
+ .isEqualTo(ParameterType.String to "")
}
}
@Test
fun testShadow() {
assertThat(lookup(Shadow.None)).isEqualTo(ParameterType.String to "None")
- validate(factory.create(node, "shadow", Shadow(Color.Cyan, Offset.Zero, 2.5f))!!) {
+ validate(create("shadow", Shadow(Color.Cyan, Offset.Zero, 2.5f))) {
parameter("shadow", ParameterType.String, Shadow::class.java.simpleName) {
parameter("blurRadius", ParameterType.DimensionDp, 1.25f)
parameter("color", ParameterType.Color, Color.Cyan.toArgb())
parameter("offset", ParameterType.String, "Zero")
}
}
- validate(factory.create(node, "shadow", Shadow(Color.Blue, Offset(1.0f, 4.0f), 1.5f))!!) {
+ validate(create("shadow", Shadow(Color.Blue, Offset(1.0f, 4.0f), 1.5f))) {
parameter("shadow", ParameterType.String, Shadow::class.java.simpleName) {
parameter("blurRadius", ParameterType.DimensionDp, 0.75f)
parameter("color", ParameterType.Color, Color.Blue.toArgb())
@@ -610,7 +742,7 @@
@Test
fun testTextGeometricTransform() {
- validate(factory.create(node, "transform", TextGeometricTransform(2.0f, 1.5f))!!) {
+ validate(create("transform", TextGeometricTransform(2.0f, 1.5f))) {
parameter(
"transform", ParameterType.String,
TextGeometricTransform::class.java.simpleName
@@ -625,7 +757,7 @@
fun testTextIndent() {
assertThat(lookup(TextIndent.None)).isEqualTo(ParameterType.String to "None")
- validate(factory.create(node, "textIndent", TextIndent(4.0.sp, 0.5.sp))!!) {
+ validate(create("textIndent", TextIndent(4.0.sp, 0.5.sp))) {
parameter("textIndent", ParameterType.String, "TextIndent") {
parameter("firstLine", ParameterType.DimensionSp, 4.0f)
parameter("restLine", ParameterType.DimensionSp, 0.5f)
@@ -639,14 +771,14 @@
color = Color.Red,
textDecoration = TextDecoration.Underline
)
- validate(factory.create(node, "style", style)!!) {
+ validate(create("style", style)) {
parameter("style", ParameterType.String, TextStyle::class.java.simpleName) {
parameter("background", ParameterType.String, "Unspecified")
- parameter("color", ParameterType.Color, Color.Red.toArgb())
- parameter("fontSize", ParameterType.String, "Unspecified")
- parameter("letterSpacing", ParameterType.String, "Unspecified")
- parameter("lineHeight", ParameterType.String, "Unspecified")
- parameter("textDecoration", ParameterType.String, "Underline")
+ parameter("color", ParameterType.Color, Color.Red.toArgb(), index = 2)
+ parameter("fontSize", ParameterType.String, "Unspecified", index = 5)
+ parameter("letterSpacing", ParameterType.String, "Unspecified", index = 9)
+ parameter("lineHeight", ParameterType.String, "Unspecified", index = 10)
+ parameter("textDecoration", ParameterType.String, "Underline", index = 14)
}
}
}
@@ -670,18 +802,57 @@
assertThat(lookup(Icons.Rounded.Add)).isEqualTo(ParameterType.String to "Rounded.Add")
}
- private fun lookup(value: Any): Pair<ParameterType, Any?>? {
- val parameter = factory.create(node, "property", value) ?: return null
+ private fun create(name: String, value: Any): NodeParameter {
+ val parameter = factory.create(ROOT_ID, node, name, value, PARAM_INDEX)
+
+ // Check that factory.expand will return the exact same information as factory.create
+ // for each parameter and parameter child. Punt if there are references.
+ checkExpand(parameter, parameter.name, value, mutableListOf())
+
+ return parameter
+ }
+
+ private fun lookup(value: Any): Pair<ParameterType, Any?> {
+ val parameter = create("parameter", value)
assertThat(parameter.elements).isEmpty()
return Pair(parameter.type, parameter.value)
}
+ private fun ref(vararg reference: Int): NodeParameterReference =
+ NodeParameterReference(NODE_ID, PARAM_INDEX, reference)
+
private fun validate(
parameter: NodeParameter,
expected: ParameterValidationReceiver.() -> Unit = {}
) {
val elements = ParameterValidationReceiver(listOf(parameter).listIterator())
elements.expected()
+ elements.checkFinished()
+ }
+
+ private fun checkExpand(
+ parameter: NodeParameter,
+ name: String,
+ value: Any,
+ indices: MutableList<Int>
+ ) {
+ factory.clearCacheFor(ROOT_ID)
+ val reference = NodeParameterReference(NODE_ID, PARAM_INDEX, indices)
+ val expanded = factory.expand(ROOT_ID, node, name, value, reference)
+ if (parameter.value == null && indices.isNotEmpty()) {
+ assertThat(expanded).isNull()
+ } else {
+ val hasReferences = expanded!!.checkEquals(parameter)
+ if (!hasReferences) {
+ parameter.elements.forEach { element ->
+ if (element.index >= 0) {
+ indices.add(element.index)
+ checkExpand(element, name, value, indices)
+ indices.removeLast()
+ }
+ }
+ }
+ }
}
}
@@ -705,37 +876,71 @@
}
}
-class ParameterValidationReceiver(val parameterIterator: Iterator<NodeParameter>) {
+class ParameterValidationReceiver(
+ private val parameterIterator: Iterator<NodeParameter>,
+ private val trace: String = ""
+) {
fun parameter(
name: String,
type: ParameterType,
value: Any?,
+ ref: NodeParameterReference? = null,
+ index: Int = -1,
block: ParameterValidationReceiver.() -> Unit = {}
) {
assertWithMessage("No such element found: $name").that(parameterIterator.hasNext()).isTrue()
val parameter = parameterIterator.next()
assertThat(parameter.name).isEqualTo(name)
- assertWithMessage(name).that(parameter.type).isEqualTo(type)
+ val msg = "$trace${parameter.name}"
+ assertWithMessage(msg).that(parameter.type).isEqualTo(type)
+ assertWithMessage(msg).that(parameter.index).isEqualTo(index)
+ assertWithMessage(msg).that(checkEquals(parameter.reference, ref)).isTrue()
if (type != ParameterType.Lambda || value != null) {
- assertWithMessage(name).that(parameter.value).isEqualTo(value)
+ assertWithMessage(msg).that(parameter.value).isEqualTo(value)
}
var elements: List<NodeParameter> = parameter.elements
- if (name != "modifier") {
- // Do not sort modifiers: the order is important
+ if (name != "modifier" && type != ParameterType.Iterable) {
+ // Do not sort modifiers or iterables: the order is important
elements = elements.sortedBy { it.name }
}
- val children = ParameterValidationReceiver(elements.listIterator())
+ val children = ParameterValidationReceiver(elements.listIterator(), "$msg.")
children.block()
- if (children.parameterIterator.hasNext()) {
+ children.checkFinished(msg)
+ }
+
+ fun checkFinished(trace: String = "") {
+ if (parameterIterator.hasNext()) {
val elementNames = mutableListOf<String>()
- while (children.parameterIterator.hasNext()) {
- elementNames.add(children.parameterIterator.next().name)
+ while (parameterIterator.hasNext()) {
+ elementNames.add(parameterIterator.next().name)
}
- error("$name: has more elements like: ${elementNames.joinToString()}")
+ error("$trace: has more elements like: ${elementNames.joinToString()}")
}
}
}
-class MyClass {
+@Suppress("unused")
+class MyClass(private val name: String) {
var other: MyClass? = null
+ var self: MyClass? = null
+ var third: MyClass? = null
}
+
+private fun NodeParameter.checkEquals(other: NodeParameter): Boolean {
+ assertThat(other.name).isEqualTo(name)
+ assertThat(other.type).isEqualTo(type)
+ assertThat(other.value).isEqualTo(value)
+ assertThat(checkEquals(reference, other.reference)).isTrue()
+ assertThat(other.elements.size).isEqualTo(elements.size)
+ var hasReferences = reference != null
+ elements.forEachIndexed { i, element ->
+ hasReferences = hasReferences or element.checkEquals(other.elements[i])
+ }
+ return hasReferences
+}
+
+private fun checkEquals(ref1: NodeParameterReference?, ref2: NodeParameterReference?): Boolean =
+ ref1 === ref2 ||
+ ref1?.nodeId == ref2?.nodeId &&
+ ref1?.parameterIndex == ref2?.parameterIndex &&
+ ref1?.indices.contentEquals(ref2?.indices)
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/ParametersTestActivity.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/ParametersTestActivity.kt
index dc83632..dc2af17 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/ParametersTestActivity.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/ParametersTestActivity.kt
@@ -26,6 +26,7 @@
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.inspection.test.R
+import androidx.compose.runtime.Composable
class ParametersTestActivity : ComponentActivity() {
private val fontFamily = FontFamily(
@@ -50,8 +51,15 @@
Button(onClick = ::testClickHandler) {
Text("two", fontFamily = fontFamily)
}
+ FunctionWithIntArray(intArrayOf(10, 11, 12, 13, 14, 15, 16, 17))
}
}
}
+@Suppress("UNUSED_PARAMETER")
+@Composable
+fun FunctionWithIntArray(intArray: IntArray) {
+ Text("three")
+}
+
internal fun testClickHandler() {}
\ No newline at end of file
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/util/ProtoExtensions.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/util/ProtoExtensions.kt
index 1b71a61..5cc8b60 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/util/ProtoExtensions.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/util/ProtoExtensions.kt
@@ -20,6 +20,8 @@
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Command
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetComposablesCommand
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetParametersCommand
+import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetParameterDetailsCommand
+import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.ParameterReference
fun List<LayoutInspectorComposeProtocol.StringEntry>.toMap() = associate { it.id to it.str }
@@ -35,6 +37,26 @@
}.build()
}.build()
+fun GetParameterDetailsCommand(
+ rootViewId: Long,
+ reference: ParameterReference,
+ startIndex: Int,
+ maxElements: Int,
+ skipSystemComposables: Boolean = true
+) = Command.newBuilder().apply {
+ getParameterDetailsCommand = GetParameterDetailsCommand.newBuilder().apply {
+ this.rootViewId = rootViewId
+ this.skipSystemComposables = skipSystemComposables
+ this.reference = reference
+ if (startIndex >= 0) {
+ this.startIndex = startIndex
+ }
+ if (maxElements >= 0) {
+ this.maxElements = maxElements
+ }
+ }.build()
+}.build()
+
fun GetComposablesCommand(rootViewId: Long, skipSystemComposables: Boolean = true) =
Command.newBuilder().apply {
getComposablesCommand = GetComposablesCommand.newBuilder()
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt
index 4080ce9..f8e0b80 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/ComposeLayoutInspector.kt
@@ -24,22 +24,29 @@
import androidx.compose.ui.inspection.framework.flatten
import androidx.compose.ui.inspection.inspector.InspectorNode
import androidx.compose.ui.inspection.inspector.LayoutInspectorTree
+import androidx.compose.ui.inspection.inspector.NodeParameterReference
import androidx.compose.ui.inspection.proto.StringTable
+import androidx.compose.ui.inspection.proto.convert
import androidx.compose.ui.inspection.proto.convertAll
import androidx.compose.ui.inspection.util.ThreadUtils
import androidx.inspection.Connection
import androidx.inspection.Inspector
import androidx.inspection.InspectorEnvironment
import androidx.inspection.InspectorFactory
+import com.google.protobuf.ByteString
+import com.google.protobuf.InvalidProtocolBufferException
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Command
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetAllParametersCommand
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetAllParametersResponse
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetComposablesCommand
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetComposablesResponse
+import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetParameterDetailsCommand
+import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetParameterDetailsResponse
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetParametersCommand
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.GetParametersResponse
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.ParameterGroup
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Response
+import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.UnknownCommandResponse
private const val LAYOUT_INSPECTION_ID = "layoutinspector.compose.inspection"
@@ -79,7 +86,13 @@
}
override fun onReceiveCommand(data: ByteArray, callback: CommandCallback) {
- val command = Command.parseFrom(data)
+ val command = try {
+ Command.parseFrom(data)
+ } catch (ignored: InvalidProtocolBufferException) {
+ handleUnknownCommand(data, callback)
+ return
+ }
+
when (command.specializedCase) {
Command.SpecializedCase.GET_COMPOSABLES_COMMAND -> {
handleGetComposablesCommand(command.getComposablesCommand, callback)
@@ -90,7 +103,18 @@
Command.SpecializedCase.GET_ALL_PARAMETERS_COMMAND -> {
handleGetAllParametersCommand(command.getAllParametersCommand, callback)
}
- else -> error("Unexpected compose inspector command case: ${command.specializedCase}")
+ Command.SpecializedCase.GET_PARAMETER_DETAILS_COMMAND -> {
+ handleGetParameterDetailsCommand(command.getParameterDetailsCommand, callback)
+ }
+ else -> handleUnknownCommand(data, callback)
+ }
+ }
+
+ private fun handleUnknownCommand(commandBytes: ByteArray, callback: CommandCallback) {
+ callback.reply {
+ unknownCommandResponse = UnknownCommandResponse.newBuilder().apply {
+ this.commandBytes = ByteString.copyFrom(commandBytes)
+ }.build()
}
}
@@ -137,11 +161,13 @@
getParametersCommand.skipSystemComposables
)[getParametersCommand.composableId]
+ val rootId = getParametersCommand.rootViewId
+
callback.reply {
getParametersResponse = if (foundComposable != null) {
val stringTable = StringTable()
- val parameters =
- foundComposable.convertParameters(layoutInspectorTree).convertAll(stringTable)
+ val parameters = foundComposable.convertParameters(layoutInspectorTree, rootId)
+ .convertAll(stringTable)
GetParametersResponse.newBuilder().apply {
parameterGroup = ParameterGroup.newBuilder().apply {
composableId = getParametersCommand.composableId
@@ -165,11 +191,13 @@
getAllParametersCommand.skipSystemComposables
).values
+ val rootId = getAllParametersCommand.rootViewId
+
callback.reply {
val stringTable = StringTable()
val parameterGroups = allComposables.map { composable ->
- val parameters =
- composable.convertParameters(layoutInspectorTree).convertAll(stringTable)
+ val parameters = composable.convertParameters(layoutInspectorTree, rootId)
+ .convertAll(stringTable)
ParameterGroup.newBuilder().apply {
composableId = composable.id
addAllParameter(parameters)
@@ -177,13 +205,50 @@
}
getAllParametersResponse = GetAllParametersResponse.newBuilder().apply {
- rootViewId = getAllParametersCommand.rootViewId
+ rootViewId = rootId
addAllParameterGroups(parameterGroups)
addAllStrings(stringTable.toStringEntries())
}.build()
}
}
+ private fun handleGetParameterDetailsCommand(
+ getParameterDetailsCommand: GetParameterDetailsCommand,
+ callback: CommandCallback
+ ) {
+ val composables = getComposableNodes(
+ getParameterDetailsCommand.rootViewId,
+ getParameterDetailsCommand.skipSystemComposables
+ )
+ val reference = NodeParameterReference(
+ getParameterDetailsCommand.reference.composableId,
+ getParameterDetailsCommand.reference.parameterIndex,
+ getParameterDetailsCommand.reference.compositeIndexList
+ )
+ val expanded = composables[reference.nodeId]?.let { composable ->
+ layoutInspectorTree.expandParameter(
+ getParameterDetailsCommand.rootViewId,
+ composable,
+ reference,
+ getParameterDetailsCommand.startIndex,
+ getParameterDetailsCommand.maxElements
+ )
+ }
+
+ callback.reply {
+ getParameterDetailsResponse = if (expanded != null) {
+ val stringTable = StringTable()
+ GetParameterDetailsResponse.newBuilder().apply {
+ rootViewId = getParameterDetailsCommand.rootViewId
+ parameter = expanded.convert(stringTable)
+ addAllStrings(stringTable.toStringEntries())
+ }.build()
+ } else {
+ GetParameterDetailsResponse.getDefaultInstance()
+ }
+ }
+ }
+
/**
* Get all [InspectorNode]s found under the layout tree rooted by [rootViewId]. They will be
* mapped with their ID as the key.
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/compose/ComposeExtensions.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/compose/ComposeExtensions.kt
index a655403..f98479d 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/compose/ComposeExtensions.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/compose/ComposeExtensions.kt
@@ -27,9 +27,12 @@
* This method can take a long time, especially the first time, and should be called off the main
* thread.
*/
-fun InspectorNode.convertParameters(layoutInspectorTree: LayoutInspectorTree): List<NodeParameter> {
+fun InspectorNode.convertParameters(
+ layoutInspectorTree: LayoutInspectorTree,
+ rootId: Long
+): List<NodeParameter> {
ThreadUtils.assertOffMainThread()
- return layoutInspectorTree.convertParameters(this)
+ return layoutInspectorTree.convertParameters(rootId, this)
}
/**
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
index 7138052..43b8e2d 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
@@ -110,8 +110,37 @@
/**
* Converts the [RawParameter]s of the [node] into displayable parameters.
*/
- fun convertParameters(node: InspectorNode): List<NodeParameter> {
- return node.parameters.mapNotNull { parameterFactory.create(node, it.name, it.value) }
+ fun convertParameters(rootId: Long, node: InspectorNode): List<NodeParameter> {
+ return node.parameters.mapIndexed { index, parameter ->
+ parameterFactory.create(rootId, node, parameter.name, parameter.value, index)
+ }
+ }
+
+ /**
+ * Converts a part of the [RawParameter] identified by [reference] into a
+ * displayable parameter. If the parameter is some sort of a collection
+ * then [startIndex] and [maxElements] describes the scope of the data returned.
+ */
+ fun expandParameter(
+ rootId: Long,
+ node: InspectorNode,
+ reference: NodeParameterReference,
+ startIndex: Int,
+ maxElements: Int
+ ): NodeParameter? {
+ if (reference.parameterIndex !in node.parameters.indices) {
+ return null
+ }
+ val parameter = node.parameters[reference.parameterIndex]
+ return parameterFactory.expand(
+ rootId,
+ node,
+ parameter.name,
+ parameter.value,
+ reference,
+ startIndex,
+ maxElements
+ )
}
/**
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/NodeParameter.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/NodeParameter.kt
index afa1d8a..f9057e6 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/NodeParameter.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/NodeParameter.kt
@@ -39,6 +39,18 @@
* Sub elements of the parameter.
*/
val elements = mutableListOf<NodeParameter>()
+
+ /**
+ * Reference to value parameter.
+ */
+ var reference: NodeParameterReference? = null
+
+ /**
+ * The index into the composite parent parameter value.
+ *
+ * If the index is identical to index of the parent element list then this value will be -1.
+ */
+ var index = -1
}
/**
@@ -58,4 +70,5 @@
DimensionEm,
Lambda,
FunctionReference,
+ Iterable,
}
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/NodeParameterReference.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/NodeParameterReference.kt
new file mode 100644
index 0000000..de5268d
--- /dev/null
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/NodeParameterReference.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.inspection.inspector
+
+import androidx.compose.ui.inspection.util.asIntArray
+
+/**
+ * A reference to a parameter to a [NodeParameter]
+ *
+ * @param nodeId is the id of the node the parameter belongs to
+ * @param parameterIndex is the parameter index among [InspectorNode.parameters]
+ * @param indices are indices into the composite parameter
+ */
+class NodeParameterReference(
+ val nodeId: Long,
+ val parameterIndex: Int,
+ val indices: IntArray
+) {
+ constructor (
+ nodeId: Long,
+ parameterIndex: Int,
+ indices: List<Int>
+ ) : this(nodeId, parameterIndex, indices.asIntArray())
+}
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/ParameterFactory.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/ParameterFactory.kt
index 282b790..4d3d165 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/ParameterFactory.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/ParameterFactory.kt
@@ -37,18 +37,20 @@
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.ResourceFont
import androidx.compose.ui.text.intl.Locale
-import androidx.compose.ui.text.intl.LocaleList
import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
+import org.jetbrains.annotations.TestOnly
import java.lang.reflect.Field
+import java.util.IdentityHashMap
import kotlin.jvm.internal.FunctionReference
import kotlin.jvm.internal.Lambda
import kotlin.math.abs
import kotlin.reflect.KClass
+import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
import kotlin.reflect.full.allSuperclasses
import kotlin.reflect.full.declaredMemberProperties
@@ -57,8 +59,8 @@
import kotlin.reflect.jvm.javaGetter
import java.lang.reflect.Modifier as JavaModifier
-private const val MAX_RECURSIONS = 10
-private const val MAX_ITERABLE = 25
+private const val MAX_RECURSIONS = 2
+private const val MAX_ITERABLE_SIZE = 5
private val reflectionScope: ReflectionScope = ReflectionScope()
@@ -98,6 +100,12 @@
var density = Density(1.0f)
+ @set:TestOnly
+ var maxRecursions = MAX_RECURSIONS
+
+ @set:TestOnly
+ var maxIterable = MAX_ITERABLE_SIZE
+
init {
val textDecorationCombination = TextDecoration.combine(
listOf(TextDecoration.LineThrough, TextDecoration.Underline)
@@ -121,17 +129,56 @@
* Attempt to convert the value to a user readable value.
* For now: return null when a conversion is not possible/found.
*/
- fun create(node: InspectorNode, name: String, value: Any?): NodeParameter? {
+ fun create(
+ rootId: Long,
+ node: InspectorNode,
+ name: String,
+ value: Any?,
+ parameterIndex: Int
+ ): NodeParameter {
val creator = creatorCache ?: ParameterCreator()
try {
return reflectionScope.withReflectiveAccess {
- creator.create(node, name, value)
+ creator.create(rootId, node, name, value, parameterIndex)
}
} finally {
creatorCache = creator
}
}
+ /**
+ * Create/expand the [NodeParameter] specified by [reference].
+ *
+ * @param node is the [InspectorNode] with the id of [reference].nodeId.
+ * @param name is the name of the [reference].parameterIndex'th parameter of [node].
+ * @param value is the value of the [reference].parameterIndex'th parameter of [node].
+ * @param startIndex is the index of the 1st wanted element of a List/Array.
+ * @param maxElements is the max number of elements wanted from a List/Array.
+ */
+ fun expand(
+ rootId: Long,
+ node: InspectorNode,
+ name: String,
+ value: Any?,
+ reference: NodeParameterReference,
+ startIndex: Int = 0,
+ maxElements: Int = maxIterable
+ ): NodeParameter? {
+ val creator = creatorCache ?: ParameterCreator()
+ try {
+ return reflectionScope.withReflectiveAccess {
+ creator.expand(rootId, node, name, value, reference, startIndex, maxElements)
+ }
+ } finally {
+ creatorCache = creator
+ }
+ }
+
+ fun clearCacheFor(rootId: Long) {
+ val creator = creatorCache ?: return
+ creator.clearCacheFor(rootId)
+ }
+
private fun loadConstantsFrom(javaClass: Class<*>) {
if (valuesLoaded.contains(javaClass) ||
ignoredPackagePrefixes.any { javaClass.name.startsWith(it) }
@@ -253,62 +300,250 @@
* Convenience class for building [NodeParameter]s.
*/
private inner class ParameterCreator {
+ private var rootId = 0L
private var node: InspectorNode? = null
+ private var parameterIndex = 0
private var recursions = 0
+ private val valueIndex = mutableListOf<Int>()
+ private val valueLazyReferenceMap = IdentityHashMap<Any, MutableList<NodeParameter>>()
+ private val rootValueIndexCache =
+ mutableMapOf<Long, IdentityHashMap<Any, NodeParameterReference>>()
+ private var valueIndexMap = IdentityHashMap<Any, NodeParameterReference>()
- fun create(node: InspectorNode, name: String, value: Any?): NodeParameter? = try {
- this.node = node
+ fun create(
+ rootId: Long,
+ node: InspectorNode,
+ name: String,
+ value: Any?,
+ parameterIndex: Int
+ ): NodeParameter =
+ try {
+ setup(rootId, node, parameterIndex)
+ create(name, value) ?: createEmptyParameter(name)
+ } finally {
+ setup()
+ }
+
+ fun expand(
+ rootId: Long,
+ node: InspectorNode,
+ name: String,
+ value: Any?,
+ reference: NodeParameterReference,
+ startIndex: Int,
+ maxElements: Int
+ ): NodeParameter? {
+ setup(rootId, node, reference.parameterIndex)
+ var new = Pair(name, value)
+ for (i in reference.indices) {
+ new = find(new.first, new.second, i) ?: return null
+ }
recursions = 0
- create(name, value)
- } finally {
- this.node = null
+ valueIndex.addAll(reference.indices.asSequence())
+ val parameter = if (startIndex == 0) {
+ create(new.first, new.second)
+ } else {
+ createFromCompositeValue(new.first, new.second, startIndex, maxElements)
+ }
+ if (parameter == null && reference.indices.isEmpty()) {
+ return createEmptyParameter(name)
+ }
+ return parameter
+ }
+
+ fun clearCacheFor(rootId: Long) {
+ rootValueIndexCache.remove(rootId)
+ }
+
+ private fun setup(
+ newRootId: Long = 0,
+ newNode: InspectorNode? = null,
+ newParameterIndex: Int = 0
+ ) {
+ rootId = newRootId
+ node = newNode
+ parameterIndex = newParameterIndex
+ recursions = 0
+ valueIndex.clear()
+ valueLazyReferenceMap.clear()
+ valueIndexMap = rootValueIndexCache.getOrPut(newRootId) {
+ IdentityHashMap()
+ }
}
private fun create(name: String, value: Any?): NodeParameter? {
- if (value == null || recursions >= MAX_RECURSIONS) {
+ if (value == null) {
return null
}
- try {
- recursions++
- createFromConstant(name, value)?.let { return it }
- @OptIn(ComposeCompilerApi::class)
- return when (value) {
- is AnnotatedString -> NodeParameter(name, ParameterType.String, value.text)
- is BaselineShift -> createFromBaselineShift(name, value)
- is Boolean -> NodeParameter(name, ParameterType.Boolean, value)
- is ComposableLambda -> createFromCLambda(name, value)
- is Color -> NodeParameter(name, ParameterType.Color, value.toArgb())
-// is CornerSize -> createFromCornerSize(name, value)
- is Double -> NodeParameter(name, ParameterType.Double, value)
- is Dp -> NodeParameter(name, DimensionDp, value.value)
- is Enum<*> -> NodeParameter(name, ParameterType.String, value.toString())
- is Float -> NodeParameter(name, ParameterType.Float, value)
- is FunctionReference -> NodeParameter(
- name, ParameterType.FunctionReference, arrayOf<Any>(value, value.name)
- )
- is FontListFontFamily -> createFromFontListFamily(name, value)
- is FontWeight -> NodeParameter(name, ParameterType.Int32, value.weight)
- is Modifier -> createFromModifier(name, value)
- is InspectableValue -> createFromInspectableValue(name, value)
- is Int -> NodeParameter(name, ParameterType.Int32, value)
- is Iterable<*> -> createFromIterable(name, value)
- is Lambda<*> -> createFromLambda(name, value)
- is Locale -> NodeParameter(name, ParameterType.String, value.toString())
- is LocaleList ->
- NodeParameter(name, ParameterType.String, value.localeList.joinToString())
- is Long -> NodeParameter(name, ParameterType.Int64, value)
- is Offset -> createFromOffset(name, value)
- is Shadow -> createFromShadow(name, value)
- is SolidColor -> NodeParameter(name, ParameterType.Color, value.value.toArgb())
- is String -> NodeParameter(name, ParameterType.String, value)
- is TextUnit -> createFromTextUnit(name, value)
- is ImageVector -> createFromImageVector(name, value)
- is View -> NodeParameter(name, ParameterType.String, value.javaClass.simpleName)
- else -> createFromKotlinReflection(name, value)
- }
- } finally {
- recursions--
+ createFromSimpleValue(name, value)?.let { return it }
+
+ val existing = valueIndexMap[value] ?: return createFromCompositeValue(name, value)
+
+ // Do not decompose an instance we already decomposed.
+ // Instead reference the data that was already decomposed.
+ return createReferenceToExistingValue(name, value, existing)
+ }
+
+ private fun createFromSimpleValue(name: String, value: Any?): NodeParameter? {
+ if (value == null) {
+ return null
}
+ createFromConstant(name, value)?.let { return it }
+ @OptIn(ComposeCompilerApi::class)
+ return when (value) {
+ is AnnotatedString -> NodeParameter(name, ParameterType.String, value.text)
+ is BaselineShift -> createFromBaselineShift(name, value)
+ is Boolean -> NodeParameter(name, ParameterType.Boolean, value)
+ is ComposableLambda -> createFromCLambda(name, value)
+ is Color -> NodeParameter(name, ParameterType.Color, value.toArgb())
+// is CornerSize -> createFromCornerSize(name, value)
+ is Double -> NodeParameter(name, ParameterType.Double, value)
+ is Dp -> NodeParameter(name, DimensionDp, value.value)
+ is Enum<*> -> NodeParameter(name, ParameterType.String, value.toString())
+ is Float -> NodeParameter(name, ParameterType.Float, value)
+ is FunctionReference -> createFromFunctionReference(name, value)
+ is FontListFontFamily -> createFromFontListFamily(name, value)
+ is FontWeight -> NodeParameter(name, ParameterType.Int32, value.weight)
+ is Int -> NodeParameter(name, ParameterType.Int32, value)
+ is Lambda<*> -> createFromLambda(name, value)
+ is Locale -> NodeParameter(name, ParameterType.String, value.toString())
+ is Long -> NodeParameter(name, ParameterType.Int64, value)
+ is Offset -> createFromOffset(name, value)
+ is SolidColor -> NodeParameter(name, ParameterType.Color, value.value.toArgb())
+ is String -> NodeParameter(name, ParameterType.String, value)
+ is TextUnit -> createFromTextUnit(name, value)
+ is ImageVector -> createFromImageVector(name, value)
+ is View -> NodeParameter(name, ParameterType.String, value.javaClass.simpleName)
+ else -> null
+ }
+ }
+
+ private fun createFromCompositeValue(
+ name: String,
+ value: Any?,
+ startIndex: Int = 0,
+ maxElements: Int = maxIterable
+ ): NodeParameter? = when {
+ value == null -> null
+ value is Modifier -> createFromModifier(name, value)
+ value is InspectableValue -> createFromInspectableValue(name, value)
+ value is Sequence<*> ->
+ createFromSequence(name, value, value, startIndex, maxElements)
+ value is Iterable<*> ->
+ createFromSequence(name, value, value.asSequence(), startIndex, maxElements)
+ value.javaClass.isArray -> createFromArray(name, value, startIndex, maxElements)
+ value is Shadow -> createFromShadow(name, value)
+ else -> createFromKotlinReflection(name, value)
+ }
+
+ private fun find(name: String, value: Any?, index: Int): Pair<String, Any?>? = when {
+ value == null -> null
+ value is Modifier -> findFromModifier(name, value, index)
+ value is InspectableValue -> findFromInspectableValue(value, index)
+ value is Sequence<*> -> findFromSequence(value, index)
+ value is Iterable<*> -> findFromSequence(value.asSequence(), index)
+ value.javaClass.isArray -> findFromArray(value, index)
+ value is Shadow -> findFromShadow(value, index)
+ else -> findFromKotlinReflection(value, index)
+ }
+
+ private fun createRecursively(
+ name: String,
+ value: Any?,
+ index: Int,
+ elementsIndex: Int
+ ): NodeParameter? {
+ valueIndex.add(index)
+ recursions++
+ val parameter = create(name, value)?.apply {
+ this.index = if (index != elementsIndex) index else -1
+ }
+ recursions--
+ valueIndex.removeLast()
+ return parameter
+ }
+
+ private fun shouldRecurseDeeper(): Boolean =
+ recursions < maxRecursions
+
+ /**
+ * Create a [NodeParameter] as a reference to a previously created parameter.
+ *
+ * Use [createFromCompositeValue] to compute the data type and top value,
+ * however no children will be created. Instead a reference to the previously
+ * created parameter is specified.
+ */
+ private fun createReferenceToExistingValue(
+ name: String,
+ value: Any?,
+ ref: NodeParameterReference
+ ): NodeParameter? {
+ val remember = recursions
+ recursions = maxRecursions
+ val parameter = createFromCompositeValue(name, value)?.apply { reference = ref }
+ recursions = remember
+ return parameter
+ }
+
+ /**
+ * Store the reference of this [NodeParameter] by its [value]
+ *
+ * If the value is seen in other parameter values again, there is
+ * no need to create child parameters a second time.
+ */
+ private fun NodeParameter.store(value: Any?): NodeParameter {
+ if (value != null) {
+ val index = valueIndexToReference()
+ valueIndexMap[value] = index
+ valueLazyReferenceMap.remove(value)?.forEach { it.reference = index }
+ }
+ return this
+ }
+
+ /**
+ * Delay the creation of all child parameters of this composite parameter.
+ *
+ * If the child parameters are omitted because of [maxRecursions], store the
+ * parameter itself such that its reference can be updated if it turns out
+ * that child [NodeParameter]s need to be generated later.
+ */
+ private fun NodeParameter.withChildReference(value: Any): NodeParameter {
+ valueLazyReferenceMap.getOrPut(value, { mutableListOf() }).add(this)
+ reference = valueIndexToReference()
+ return this
+ }
+
+ private fun valueIndexToReference(): NodeParameterReference =
+ NodeParameterReference(node!!.id, parameterIndex, valueIndex)
+
+ private fun createEmptyParameter(name: String): NodeParameter =
+ NodeParameter(name, ParameterType.String, "")
+
+ private fun createFromArray(
+ name: String,
+ value: Any,
+ startIndex: Int,
+ maxElements: Int
+ ): NodeParameter? {
+ val sequence = arrayToSequence(value) ?: return null
+ return createFromSequence(name, value, sequence, startIndex, maxElements)
+ }
+
+ private fun findFromArray(value: Any, index: Int): Pair<String, Any?>? {
+ val sequence = arrayToSequence(value) ?: return null
+ return findFromSequence(sequence, index)
+ }
+
+ private fun arrayToSequence(value: Any): Sequence<*>? = when (value) {
+ is Array<*> -> value.asSequence()
+ is ByteArray -> value.asSequence()
+ is IntArray -> value.asSequence()
+ is LongArray -> value.asSequence()
+ is FloatArray -> value.asSequence()
+ is DoubleArray -> value.asSequence()
+ is BooleanArray -> value.asSequence()
+ is CharArray -> value.asSequence()
+ else -> null
}
private fun createFromBaselineShift(name: String, value: BaselineShift): NodeParameter {
@@ -351,10 +586,40 @@
NodeParameter(name, ParameterType.Resource, it.resId)
}
+ private fun createFromFunctionReference(
+ name: String,
+ value: FunctionReference
+ ): NodeParameter =
+ NodeParameter(name, ParameterType.FunctionReference, arrayOf<Any>(value, value.name))
+
private fun createFromKotlinReflection(name: String, value: Any): NodeParameter? {
+ val simpleName = value::class.simpleName
+ val properties = lookup(value) ?: return null
+ val parameter = NodeParameter(name, ParameterType.String, simpleName)
+ return when {
+ properties.isEmpty() -> parameter
+ !shouldRecurseDeeper() -> parameter.withChildReference(value)
+ else -> {
+ val elements = parameter.store(value).elements
+ properties.values.mapIndexedNotNullTo(elements) { index, part ->
+ createRecursively(part.name, valueOf(part, value), index, elements.size)
+ }
+ parameter
+ }
+ }
+ }
+
+ private fun findFromKotlinReflection(value: Any, index: Int): Pair<String, Any?>? {
+ val properties = lookup(value)?.entries?.iterator()?.asSequence() ?: return null
+ val element = properties.elementAtOrNull(index)?.value ?: return null
+ return Pair(element.name, valueOf(element, value))
+ }
+
+ private fun lookup(value: Any): Map<String, KProperty<*>>? {
val kClass = value::class
+ val simpleName = kClass.simpleName
val qualifiedName = kClass.qualifiedName
- if (kClass.simpleName == null ||
+ if (simpleName == null ||
qualifiedName == null ||
ignoredPackagePrefixes.any { qualifiedName.startsWith(it) }
) {
@@ -363,28 +628,21 @@
// - certain android packages
return null
}
- val parameter = NodeParameter(name, ParameterType.String, kClass.simpleName)
- val properties = mutableMapOf<String, KProperty1<Any, *>>()
- try {
+ return try {
sequenceOf(kClass).plus(kClass.allSuperclasses.asSequence())
.flatMap { it.declaredMemberProperties.asSequence() }
- .filterIsInstance<KProperty1<Any, *>>()
- .associateByTo(properties) { it.name }
+ .associateBy { it.name }
} catch (ex: Throwable) {
Log.w("Compose", "Could not decompose ${kClass.simpleName}", ex)
- return parameter
+ null
}
- properties.values.mapNotNullTo(parameter.elements) {
- create(it.name, valueOf(it, value))
- }
- return parameter
}
- private fun valueOf(property: KProperty1<Any, *>, instance: Any): Any? = try {
+ private fun valueOf(property: KProperty<*>, instance: Any): Any? = try {
property.isAccessible = true
// Bug in kotlin reflection API: if the type is a nullable inline type with a null
// value, we get an IllegalArgumentException in this line:
- property.get(instance)
+ property.getter.call(instance)
} catch (ex: Throwable) {
// TODO: Remove this warning since this is expected with nullable inline types
Log.w("Compose", "Could not get value of ${property.name}")
@@ -398,39 +656,99 @@
val tempValue = value.valueOverride ?: ""
val parameterName = name.ifEmpty { value.nameFallback } ?: "element"
val parameterValue = if (tempValue is InspectableValue) "" else tempValue
- val parameter = create(parameterName, parameterValue)
+ val parameter = createFromSimpleValue(parameterName, parameterValue)
?: NodeParameter(parameterName, ParameterType.String, "")
- val elements = parameter.elements
- value.inspectableElements.mapNotNullTo(elements) { create(it.name, it.value) }
+ if (!shouldRecurseDeeper()) {
+ return parameter.withChildReference(value)
+ }
+ val elements = parameter.store(value).elements
+ value.inspectableElements.mapIndexedNotNullTo(elements) { index, element ->
+ createRecursively(element.name, element.value, index, elements.size)
+ }
return parameter
}
- private fun createFromIterable(name: String, value: Iterable<*>): NodeParameter {
- val parameter = NodeParameter(name, ParameterType.String, "")
- val elements = parameter.elements
- value.asSequence()
- .mapNotNull { create(elements.size.toString(), it) }
- .takeWhile { elements.size < MAX_ITERABLE }
- .toCollection(elements)
- return parameter
+ private fun findFromInspectableValue(
+ value: InspectableValue,
+ index: Int
+ ): Pair<String, Any?>? {
+ val elements = value.inspectableElements.toList()
+ if (index !in elements.indices) {
+ return null
+ }
+ val element = elements[index]
+ return Pair(element.name, element.value)
+ }
+
+ private fun createFromSequence(
+ name: String,
+ value: Any,
+ sequence: Sequence<*>,
+ startIndex: Int,
+ maxElements: Int
+ ): NodeParameter {
+ val parameter = NodeParameter(name, ParameterType.Iterable, "")
+ return when {
+ !sequence.any() -> parameter
+ !shouldRecurseDeeper() -> parameter.withChildReference(value)
+ else -> {
+ val elements = parameter.store(value).elements
+ val rest = sequence.drop(startIndex)
+ rest.take(maxElements)
+ .mapIndexedNotNullTo(elements) { i, it ->
+ val index = startIndex + i
+ createRecursively("[$index]", it, index, startIndex + elements.size)
+ }
+ if (rest.drop(maxElements).any()) {
+ parameter.withChildReference(value)
+ }
+ parameter
+ }
+ }
+ }
+
+ private fun findFromSequence(value: Sequence<*>, index: Int): Pair<String, Any?>? {
+ val element = value.elementAtOrNull(index) ?: return null
+ return Pair("[$index]", element)
}
private fun createFromLambda(name: String, value: Lambda<*>): NodeParameter =
NodeParameter(name, ParameterType.Lambda, arrayOf<Any>(value))
- private fun createFromModifier(name: String, value: Modifier): NodeParameter? =
- when {
- name.isNotEmpty() -> {
- val parameter = NodeParameter(name, ParameterType.String, "")
- val elements = parameter.elements
- value.foldIn(elements) { acc, m ->
- create("", m)?.let { param -> acc.apply { add(param) } } ?: acc
+ private fun createFromModifier(name: String, value: Modifier): NodeParameter? = when {
+ name.isNotEmpty() -> {
+ val parameter = NodeParameter(name, ParameterType.String, "")
+ val modifiers = mutableListOf<Modifier.Element>()
+ value.foldIn(modifiers) { acc, m -> acc.apply { add(m) } }
+ when {
+ modifiers.isEmpty() -> parameter
+ !shouldRecurseDeeper() -> parameter.withChildReference(value)
+ else -> {
+ val elements = parameter.elements
+ modifiers.mapIndexedNotNullTo(elements) { index, element ->
+ createRecursively("", element, index, elements.size)
+ }
+ parameter.store(value)
}
- parameter
}
- value is InspectableValue -> createFromInspectableValue(name, value)
- else -> null
}
+ value is InspectableValue -> createFromInspectableValue(name, value)
+ else -> null
+ }
+
+ private fun findFromModifier(
+ name: String,
+ value: Modifier,
+ index: Int
+ ): Pair<String, Any?>? = when {
+ name.isNotEmpty() -> {
+ val modifiers = mutableListOf<Modifier.Element>()
+ value.foldIn(modifiers) { acc, m -> acc.apply { add(m) } }
+ if (index in modifiers.indices) Pair("", modifiers[index]) else null
+ }
+ value is InspectableValue -> findFromInspectableValue(value, index)
+ else -> null
+ }
private fun createFromOffset(name: String, value: Offset): NodeParameter {
val parameter = NodeParameter(name, ParameterType.String, Offset::class.java.simpleName)
@@ -446,12 +764,22 @@
val elements = parameter.elements
val index = elements.indexOfFirst { it.name == "blurRadius" }
if (index >= 0) {
+ val existing = elements[index]
val blurRadius = with(density) { value.blurRadius.toDp().value }
elements[index] = NodeParameter("blurRadius", DimensionDp, blurRadius)
+ elements[index].index = existing.index
}
return parameter
}
+ private fun findFromShadow(value: Shadow, index: Int): Pair<String, Any?>? {
+ val result = findFromKotlinReflection(value, index)
+ if (result == null || result.first != "blurRadius") {
+ return result
+ }
+ return Pair("blurRadius", with(density) { value.blurRadius.toDp() })
+ }
+
@Suppress("DEPRECATION")
private fun createFromTextUnit(name: String, value: TextUnit): NodeParameter =
when (value.type) {
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/proto/ComposeExtensions.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/proto/ComposeExtensions.kt
index a857ff2..49db326 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/proto/ComposeExtensions.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/proto/ComposeExtensions.kt
@@ -20,11 +20,13 @@
import androidx.compose.ui.inspection.LambdaLocation
import androidx.compose.ui.inspection.inspector.InspectorNode
import androidx.compose.ui.inspection.inspector.NodeParameter
+import androidx.compose.ui.inspection.inspector.NodeParameterReference
import androidx.compose.ui.inspection.inspector.ParameterType
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Bounds
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.ComposableNode
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.LambdaValue
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Parameter
+import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.ParameterReference
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Quad
import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Rect
@@ -84,6 +86,7 @@
ParameterType.DimensionEm -> Parameter.Type.DIMENSION_EM
ParameterType.Lambda -> Parameter.Type.LAMBDA
ParameterType.FunctionReference -> Parameter.Type.FUNCTION_REFERENCE
+ ParameterType.Iterable -> Parameter.Type.ITERABLE
}
}
@@ -114,6 +117,9 @@
Parameter.Type.RESOURCE -> setResourceType(value, stringTable)
Parameter.Type.LAMBDA -> setFunctionType(value, stringTable)
Parameter.Type.FUNCTION_REFERENCE -> setFunctionType(value, stringTable)
+ Parameter.Type.ITERABLE -> {
+ // TODO: b/181899238 Support size for List and Array types
+ }
else -> error("Unknown Composable parameter type: $type")
}
}
@@ -152,7 +158,20 @@
name = stringTable.put(nodeParam.name)
type = nodeParam.type.convert()
setValue(stringTable, nodeParam.value)
- addAllElements(nodeParam.elements.map { it.convert(stringTable) })
+ index = nodeParam.index
+ nodeParam.reference?.let { reference = it.convert() }
+ if (nodeParam.elements.isNotEmpty()) {
+ addAllElements(nodeParam.elements.map { it.convert(stringTable) })
+ }
+ }.build()
+}
+
+fun NodeParameterReference.convert(): ParameterReference {
+ val reference = this
+ return ParameterReference.newBuilder().apply {
+ composableId = reference.nodeId
+ parameterIndex = reference.parameterIndex
+ addAllCompositeIndex(reference.indices.asIterable())
}.build()
}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/util/IntArray.kt
similarity index 77%
copy from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
copy to compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/util/IntArray.kt
index 7e01354..bce161e 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/util/IntArray.kt
@@ -1,5 +1,3 @@
-// ktlint-disable filename
-
/*
* Copyright 2021 The Android Open Source Project
*
@@ -16,6 +14,9 @@
* limitations under the License.
*/
-package androidx.compose.foundation
+package androidx.compose.ui.inspection.util
-internal actual typealias AtomicReference<V> = java.util.concurrent.atomic.AtomicReference<V>
\ No newline at end of file
+private val EMPTY_INT_ARRAY = intArrayOf()
+
+fun List<Int>.asIntArray() =
+ if (isNotEmpty()) toIntArray() else EMPTY_INT_ARRAY
diff --git a/compose/ui/ui-inspection/src/main/proto/compose_layout_inspection.proto b/compose/ui/ui-inspection/src/main/proto/compose_layout_inspection.proto
index 373809d..0b09814 100644
--- a/compose/ui/ui-inspection/src/main/proto/compose_layout_inspection.proto
+++ b/compose/ui/ui-inspection/src/main/proto/compose_layout_inspection.proto
@@ -121,11 +121,24 @@
DIMENSION_EM = 11;
LAMBDA = 12;
FUNCTION_REFERENCE = 13;
+ ITERABLE = 14;
}
Type type = 1;
int32 name = 2;
repeated Parameter elements = 3;
+ ParameterReference reference = 4;
+
+ // If this Parameter appears in the elements of another parameter:
+ // This index is the "natural" index of the value in the agent. If the index
+ // is identical to the elements index we do not need this value and it will
+ // be set to -1.
+ //
+ // However if some of the "sibling" values in the agent are null or cannot be
+ // decomposed, those siblings are omitted from the Parameter.elements.
+ // For all subsequent parameter elements we need to have the original index in
+ // order to find the value again using the GetParameterDetailsCommand.
+ sint32 index = 5;
oneof value {
int32 int32_value = 11;
@@ -137,6 +150,22 @@
}
}
+// A reference to a "part" of a parameter value
+message ParameterReference {
+ sint64 composable_id = 1;
+
+ // Identifies an index into a ParameterGroup
+ int32 parameter_index = 2;
+
+ // If the parameter value is a composite value such as:
+ // List<MyClass>
+ // then:
+ // composite_index[0] is the index into the List
+ // composite_index[1] is the index into the fields in MyClass
+ // composite_index[2] is the index into the field found by [1] etc...
+ repeated int32 composite_index = 3;
+}
+
// A collection of all parameters associated with a single composable
message ParameterGroup {
sint64 composable_id = 1;
@@ -145,6 +174,14 @@
// ======= COMMANDS, RESPONSES, AND EVENTS =======
+// Response fired when incoming command bytes cannot be parsed or handled. This may occur if a newer
+// version of the client tries to interact with an older inspector.
+message UnknownCommandResponse {
+ // The initial command bytes received that couldn't be handled. By returning this back to the
+ // client, it should be able to identify what they sent that failed on the inspector side.
+ bytes command_bytes = 1;
+}
+
// Request all composables found under a layout tree rooted under the specified view
message GetComposablesCommand {
int64 root_view_id = 1;
@@ -183,11 +220,27 @@
repeated ParameterGroup parameter_groups = 3;
}
+// Request parameter details for the parameter specified
+message GetParameterDetailsCommand {
+ int64 root_view_id = 1;
+ ParameterReference reference = 2;
+ int32 start_index = 3;
+ int32 max_elements = 4;
+ bool skip_system_composables = 5;
+}
+
+message GetParameterDetailsResponse {
+ int64 root_view_id = 1; // Echoed from GetParameterDetailsCommand
+ repeated StringEntry strings = 2;
+ Parameter parameter = 3;
+}
+
message Command {
oneof specialized {
GetComposablesCommand get_composables_command = 1;
GetParametersCommand get_parameters_command = 2;
GetAllParametersCommand get_all_parameters_command = 3;
+ GetParameterDetailsCommand get_parameter_details_command = 4;
}
}
@@ -196,5 +249,8 @@
GetComposablesResponse get_composables_response = 1;
GetParametersResponse get_parameters_response = 2;
GetAllParametersResponse get_all_parameters_response = 3;
+ GetParameterDetailsResponse get_parameter_details_response = 4;
+
+ UnknownCommandResponse unknown_command_response = 100;
}
}
diff --git a/compose/ui/ui-lint/lint-baseline.xml b/compose/ui/ui-lint/lint-baseline.xml
deleted file mode 100644
index 297ae16..0000000
--- a/compose/ui/ui-lint/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" version="4.2.0-beta02">
-
-</issues>
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
index 8f63900..c75e458 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
@@ -49,9 +49,11 @@
/**
* [Detector] that checks functions returning Modifiers for consistency with guidelines.
*
- * - Modifier factory functions must return Modifier as their type, and not a subclass of Modifier
- * - Modifier factory functions must be defined as an extension on Modifier to allow fluent chaining
- * - Modifier factory functions must not be marked as @Composable, and should use `composed` instead
+ * - Modifier factory functions should return Modifier as their type, and not a subclass of Modifier
+ * - Modifier factory functions should be defined as an extension on Modifier to allow fluent
+ * chaining
+ * - Modifier factory functions should not be marked as @Composable, and should use `composed`
+ * instead
*/
class ModifierDeclarationDetector : Detector(), SourceCodeScanner {
override fun getApplicableUastTypes() = listOf(UMethod::class.java)
@@ -99,7 +101,7 @@
"androidx.compose.ui.composed {} in their implementation instead of being marked " +
"as @Composable. This allows Modifiers to be referenced in top level variables " +
"and constructed outside of the composition.",
- Category.CORRECTNESS, 3, Severity.ERROR,
+ Category.CORRECTNESS, 3, Severity.WARNING,
Implementation(
ModifierDeclarationDetector::class.java,
EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
@@ -108,10 +110,10 @@
val ModifierFactoryReturnType = Issue.create(
"ModifierFactoryReturnType",
- "Modifier factory functions must return Modifier",
- "Modifier factory functions must return Modifier as their type, and not a " +
+ "Modifier factory functions should return Modifier",
+ "Modifier factory functions should return Modifier as their type, and not a " +
"subtype of Modifier (such as Modifier.Element).",
- Category.CORRECTNESS, 3, Severity.ERROR,
+ Category.CORRECTNESS, 3, Severity.WARNING,
Implementation(
ModifierDeclarationDetector::class.java,
EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
@@ -120,10 +122,10 @@
val ModifierFactoryExtensionFunction = Issue.create(
"ModifierFactoryExtensionFunction",
- "Modifier factory functions must be extensions on Modifier",
- "Modifier factory functions must be defined as extension functions on" +
+ "Modifier factory functions should be extensions on Modifier",
+ "Modifier factory functions should be defined as extension functions on" +
" Modifier to allow modifiers to be fluently chained.",
- Category.CORRECTNESS, 3, Severity.ERROR,
+ Category.CORRECTNESS, 3, Severity.WARNING,
Implementation(
ModifierDeclarationDetector::class.java,
EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
@@ -183,7 +185,7 @@
ModifierDeclarationDetector.ModifierFactoryExtensionFunction,
this,
context.getNameLocation(this),
- "Modifier factory functions must be extensions on Modifier",
+ "Modifier factory functions should be extensions on Modifier",
lintFix
)
}
@@ -247,7 +249,7 @@
ModifierFactoryReturnType,
this,
context.getNameLocation(this),
- "Modifier factory functions must have a return type of Modifier",
+ "Modifier factory functions should have a return type of Modifier",
lintFix
)
}
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierParameterDetector.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierParameterDetector.kt
index 99ba2af1..b28a255 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierParameterDetector.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierParameterDetector.kt
@@ -152,7 +152,7 @@
"- Have a type of `$ModifierShortName`" +
"- Either have no default value, or have a default value of `$ModifierShortName`" +
"- If optional, be the first optional parameter in the parameter list",
- Category.CORRECTNESS, 3, Severity.ERROR,
+ Category.CORRECTNESS, 3, Severity.WARNING,
Implementation(
ModifierParameterDetector::class.java,
EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt
index d4333ae..e7c2b4b 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierDeclarationDetectorTest.kt
@@ -62,10 +62,10 @@
.run()
.expect(
"""
-src/androidx/compose/ui/foo/TestModifier.kt:8: Error: Modifier factory functions must have a return type of Modifier [ModifierFactoryReturnType]
+src/androidx/compose/ui/foo/TestModifier.kt:8: Warning: Modifier factory functions should have a return type of Modifier [ModifierFactoryReturnType]
fun Modifier.fooModifier(): Modifier.Element {
~~~~~~~~~~~
-1 errors, 0 warnings
+0 errors, 1 warnings
"""
)
.expectFixDiffs(
@@ -105,16 +105,16 @@
.run()
.expect(
"""
-src/androidx/compose/ui/foo/TestModifier.kt:8: Error: Modifier factory functions must have a return type of Modifier [ModifierFactoryReturnType]
+src/androidx/compose/ui/foo/TestModifier.kt:8: Warning: Modifier factory functions should have a return type of Modifier [ModifierFactoryReturnType]
val Modifier.fooModifier get(): Modifier.Element {
~~~
-src/androidx/compose/ui/foo/TestModifier.kt:12: Error: Modifier factory functions must have a return type of Modifier [ModifierFactoryReturnType]
+src/androidx/compose/ui/foo/TestModifier.kt:12: Warning: Modifier factory functions should have a return type of Modifier [ModifierFactoryReturnType]
val Modifier.fooModifier2: Modifier.Element get() {
~~~
-src/androidx/compose/ui/foo/TestModifier.kt:16: Error: Modifier factory functions must have a return type of Modifier [ModifierFactoryReturnType]
+src/androidx/compose/ui/foo/TestModifier.kt:16: Warning: Modifier factory functions should have a return type of Modifier [ModifierFactoryReturnType]
val Modifier.fooModifier3: Modifier.Element get() = TestModifier
~~~
-3 errors, 0 warnings
+0 errors, 3 warnings
"""
)
.expectFixDiffs(
@@ -154,10 +154,10 @@
.run()
.expect(
"""
-src/androidx/compose/ui/foo/TestModifier.kt:8: Error: Modifier factory functions must have a return type of Modifier [ModifierFactoryReturnType]
+src/androidx/compose/ui/foo/TestModifier.kt:8: Warning: Modifier factory functions should have a return type of Modifier [ModifierFactoryReturnType]
fun Modifier.fooModifier() = TestModifier
~~~~~~~~~~~
-1 errors, 0 warnings
+0 errors, 1 warnings
"""
)
.expectFixDiffs(
@@ -189,10 +189,10 @@
.run()
.expect(
"""
-src/androidx/compose/ui/foo/TestModifier.kt:8: Error: Modifier factory functions must have a return type of Modifier [ModifierFactoryReturnType]
+src/androidx/compose/ui/foo/TestModifier.kt:8: Warning: Modifier factory functions should have a return type of Modifier [ModifierFactoryReturnType]
val Modifier.fooModifier get() = TestModifier
~~~
-1 errors, 0 warnings
+0 errors, 1 warnings
"""
)
.expectFixDiffs(
@@ -226,10 +226,10 @@
.run()
.expect(
"""
-src/androidx/compose/ui/foo/TestModifier.kt:8: Error: Modifier factory functions must have a return type of Modifier [ModifierFactoryReturnType]
+src/androidx/compose/ui/foo/TestModifier.kt:8: Warning: Modifier factory functions should have a return type of Modifier [ModifierFactoryReturnType]
fun Modifier.fooModifier(): TestModifier {
~~~~~~~~~~~
-1 errors, 0 warnings
+0 errors, 1 warnings
"""
)
.expectFixDiffs(
@@ -348,19 +348,19 @@
.run()
.expect(
"""
-src/androidx/compose/ui/foo/TestModifier.kt:8: Error: Modifier factory functions must be extensions on Modifier [ModifierFactoryExtensionFunction]
+src/androidx/compose/ui/foo/TestModifier.kt:8: Warning: Modifier factory functions should be extensions on Modifier [ModifierFactoryExtensionFunction]
fun fooModifier(): Modifier {
~~~~~~~~~~~
-src/androidx/compose/ui/foo/TestModifier.kt:12: Error: Modifier factory functions must be extensions on Modifier [ModifierFactoryExtensionFunction]
+src/androidx/compose/ui/foo/TestModifier.kt:12: Warning: Modifier factory functions should be extensions on Modifier [ModifierFactoryExtensionFunction]
val fooModifier get(): Modifier {
~~~
-src/androidx/compose/ui/foo/TestModifier.kt:16: Error: Modifier factory functions must be extensions on Modifier [ModifierFactoryExtensionFunction]
+src/androidx/compose/ui/foo/TestModifier.kt:16: Warning: Modifier factory functions should be extensions on Modifier [ModifierFactoryExtensionFunction]
val fooModifier2: Modifier get() {
~~~
-src/androidx/compose/ui/foo/TestModifier.kt:20: Error: Modifier factory functions must be extensions on Modifier [ModifierFactoryExtensionFunction]
+src/androidx/compose/ui/foo/TestModifier.kt:20: Warning: Modifier factory functions should be extensions on Modifier [ModifierFactoryExtensionFunction]
val fooModifier3: Modifier get() = TestModifier
~~~
-4 errors, 0 warnings
+0 errors, 4 warnings
"""
)
.expectFixDiffs(
@@ -416,19 +416,19 @@
.run()
.expect(
"""
-src/androidx/compose/ui/foo/TestModifier.kt:8: Error: Modifier factory functions must be extensions on Modifier [ModifierFactoryExtensionFunction]
+src/androidx/compose/ui/foo/TestModifier.kt:8: Warning: Modifier factory functions should be extensions on Modifier [ModifierFactoryExtensionFunction]
fun TestModifier.fooModifier(): Modifier {
~~~~~~~~~~~
-src/androidx/compose/ui/foo/TestModifier.kt:12: Error: Modifier factory functions must be extensions on Modifier [ModifierFactoryExtensionFunction]
+src/androidx/compose/ui/foo/TestModifier.kt:12: Warning: Modifier factory functions should be extensions on Modifier [ModifierFactoryExtensionFunction]
val TestModifier.fooModifier get(): Modifier {
~~~
-src/androidx/compose/ui/foo/TestModifier.kt:16: Error: Modifier factory functions must be extensions on Modifier [ModifierFactoryExtensionFunction]
+src/androidx/compose/ui/foo/TestModifier.kt:16: Warning: Modifier factory functions should be extensions on Modifier [ModifierFactoryExtensionFunction]
val TestModifier.fooModifier2: Modifier get() {
~~~
-src/androidx/compose/ui/foo/TestModifier.kt:20: Error: Modifier factory functions must be extensions on Modifier [ModifierFactoryExtensionFunction]
+src/androidx/compose/ui/foo/TestModifier.kt:20: Warning: Modifier factory functions should be extensions on Modifier [ModifierFactoryExtensionFunction]
val TestModifier.fooModifier3: Modifier get() = TestModifier
~~~
-4 errors, 0 warnings
+0 errors, 4 warnings
"""
)
.expectFixDiffs(
@@ -493,19 +493,19 @@
.run()
.expect(
"""
-src/androidx/compose/ui/foo/TestModifier.kt:13: Error: Modifier factory functions should not be marked as @Composable, and should use composed instead [ComposableModifierFactory]
+src/androidx/compose/ui/foo/TestModifier.kt:13: Warning: Modifier factory functions should not be marked as @Composable, and should use composed instead [ComposableModifierFactory]
fun Modifier.fooModifier1(): Modifier {
~~~~~~~~~~~~
-src/androidx/compose/ui/foo/TestModifier.kt:19: Error: Modifier factory functions should not be marked as @Composable, and should use composed instead [ComposableModifierFactory]
+src/androidx/compose/ui/foo/TestModifier.kt:19: Warning: Modifier factory functions should not be marked as @Composable, and should use composed instead [ComposableModifierFactory]
fun Modifier.fooModifier2(): Modifier = TestModifier(someComposableCall(3))
~~~~~~~~~~~~
-src/androidx/compose/ui/foo/TestModifier.kt:22: Error: Modifier factory functions should not be marked as @Composable, and should use composed instead [ComposableModifierFactory]
+src/androidx/compose/ui/foo/TestModifier.kt:22: Warning: Modifier factory functions should not be marked as @Composable, and should use composed instead [ComposableModifierFactory]
val Modifier.fooModifier3: Modifier get() {
~~~
-src/androidx/compose/ui/foo/TestModifier.kt:28: Error: Modifier factory functions should not be marked as @Composable, and should use composed instead [ComposableModifierFactory]
+src/androidx/compose/ui/foo/TestModifier.kt:28: Warning: Modifier factory functions should not be marked as @Composable, and should use composed instead [ComposableModifierFactory]
val Modifier.fooModifier4: Modifier get() = TestModifier(someComposableCall(3))
~~~
-4 errors, 0 warnings
+0 errors, 4 warnings
"""
)
.expectFixDiffs(
diff --git a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierParameterDetectorTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierParameterDetectorTest.kt
index d941ec5..f2e2aec 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierParameterDetectorTest.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierParameterDetectorTest.kt
@@ -64,10 +64,10 @@
.run()
.expect(
"""
-src/androidx/compose/ui/foo/test.kt:10: Error: Modifier parameter should be named modifier [ModifierParameter]
+src/androidx/compose/ui/foo/test.kt:10: Warning: Modifier parameter should be named modifier [ModifierParameter]
buttonModifier: Modifier = Modifier,
~~~~~~~~~~~~~~
-1 errors, 0 warnings
+0 errors, 1 warnings
"""
)
.expectFixDiffs(
@@ -105,10 +105,10 @@
.run()
.expect(
"""
-src/androidx/compose/ui/foo/test.kt:10: Error: Modifier parameter should have a type of Modifier [ModifierParameter]
+src/androidx/compose/ui/foo/test.kt:10: Warning: Modifier parameter should have a type of Modifier [ModifierParameter]
modifier: Modifier.Element,
~~~~~~~~
-1 errors, 0 warnings
+0 errors, 1 warnings
"""
)
.expectFixDiffs(
@@ -148,10 +148,10 @@
.run()
.expect(
"""
-src/androidx/compose/ui/foo/TestModifier.kt:12: Error: Optional Modifier parameter should have a default value of Modifier [ModifierParameter]
+src/androidx/compose/ui/foo/TestModifier.kt:12: Warning: Optional Modifier parameter should have a default value of Modifier [ModifierParameter]
modifier: Modifier = TestModifier,
~~~~~~~~
-1 errors, 0 warnings
+0 errors, 1 warnings
"""
)
.expectFixDiffs(
@@ -189,10 +189,10 @@
.run()
.expect(
"""
-src/androidx/compose/ui/foo/test.kt:11: Error: Modifier parameter should be the first optional parameter [ModifierParameter]
+src/androidx/compose/ui/foo/test.kt:11: Warning: Modifier parameter should be the first optional parameter [ModifierParameter]
modifier: Modifier = Modifier,
~~~~~~~~
-1 errors, 0 warnings
+0 errors, 1 warnings
"""
)
}
@@ -224,19 +224,19 @@
.run()
.expect(
"""
-src/androidx/compose/ui/foo/TestModifier.kt:13: Error: Modifier parameter should be named modifier [ModifierParameter]
+src/androidx/compose/ui/foo/TestModifier.kt:13: Warning: Modifier parameter should be named modifier [ModifierParameter]
buttonModifier: Modifier.Element = TestModifier,
~~~~~~~~~~~~~~
-src/androidx/compose/ui/foo/TestModifier.kt:13: Error: Modifier parameter should be the first optional parameter [ModifierParameter]
+src/androidx/compose/ui/foo/TestModifier.kt:13: Warning: Modifier parameter should be the first optional parameter [ModifierParameter]
buttonModifier: Modifier.Element = TestModifier,
~~~~~~~~~~~~~~
-src/androidx/compose/ui/foo/TestModifier.kt:13: Error: Modifier parameter should have a type of Modifier [ModifierParameter]
+src/androidx/compose/ui/foo/TestModifier.kt:13: Warning: Modifier parameter should have a type of Modifier [ModifierParameter]
buttonModifier: Modifier.Element = TestModifier,
~~~~~~~~~~~~~~
-src/androidx/compose/ui/foo/TestModifier.kt:13: Error: Optional Modifier parameter should have a default value of Modifier [ModifierParameter]
+src/androidx/compose/ui/foo/TestModifier.kt:13: Warning: Optional Modifier parameter should have a default value of Modifier [ModifierParameter]
buttonModifier: Modifier.Element = TestModifier,
~~~~~~~~~~~~~~
-4 errors, 0 warnings
+0 errors, 4 warnings
"""
)
.expectFixDiffs(
diff --git a/compose/ui/ui-test-font/lint-baseline.xml b/compose/ui/ui-test-font/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/ui/ui-test-font/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/ui/ui-test-junit4/lint-baseline.xml b/compose/ui/ui-test-junit4/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/ui/ui-test-junit4/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/ui/ui-test-manifest/OWNERS b/compose/ui/ui-test-manifest/OWNERS
new file mode 100644
index 0000000..42abc4e
--- /dev/null
+++ b/compose/ui/ui-test-manifest/OWNERS
@@ -0,0 +1,2 @@
+jellefresen@google.com
+pavlis@google.com
diff --git a/compose/ui/ui-test-manifest/api/1.0.0-beta02.txt b/compose/ui/ui-test-manifest/api/1.0.0-beta02.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/compose/ui/ui-test-manifest/api/1.0.0-beta02.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/compose/ui/ui-test-manifest/api/current.txt b/compose/ui/ui-test-manifest/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/compose/ui/ui-test-manifest/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/compose/ui/ui-test-manifest/api/public_plus_experimental_1.0.0-beta02.txt b/compose/ui/ui-test-manifest/api/public_plus_experimental_1.0.0-beta02.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/compose/ui/ui-test-manifest/api/public_plus_experimental_1.0.0-beta02.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/compose/ui/ui-test-manifest/api/public_plus_experimental_current.txt b/compose/ui/ui-test-manifest/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/compose/ui/ui-test-manifest/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/compose/ui/ui-test-manifest/api/res-1.0.0-beta02.txt b/compose/ui/ui-test-manifest/api/res-1.0.0-beta02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/compose/ui/ui-test-manifest/api/res-1.0.0-beta02.txt
diff --git a/compose/ui/ui-test-manifest/api/res-current.txt b/compose/ui/ui-test-manifest/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/compose/ui/ui-test-manifest/api/res-current.txt
diff --git a/compose/ui/ui-test-manifest/api/restricted_1.0.0-beta02.txt b/compose/ui/ui-test-manifest/api/restricted_1.0.0-beta02.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/compose/ui/ui-test-manifest/api/restricted_1.0.0-beta02.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/compose/ui/ui-test-manifest/api/restricted_current.txt b/compose/ui/ui-test-manifest/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/compose/ui/ui-test-manifest/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/compose/ui/ui-test-manifest/build.gradle b/compose/ui/ui-test-manifest/build.gradle
new file mode 100644
index 0000000..0c92e15
--- /dev/null
+++ b/compose/ui/ui-test-manifest/build.gradle
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+import androidx.build.AndroidXUiPlugin
+import androidx.build.LibraryGroups
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXUiPlugin")
+}
+
+dependencies {
+ api("androidx.activity:activity:1.2.0")
+}
+
+androidx {
+ name = "Compose Testing manifest dependency"
+ publish = Publish.SNAPSHOT_AND_RELEASE
+ mavenGroup = LibraryGroups.Compose.UI
+ inceptionYear = "2021"
+ description = "Compose testing library that should be added as a debugImplementation dependency to add properties to the debug manifest necessary for testing an application"
+}
diff --git a/compose/ui/ui-test-manifest/integration-tests/testapp/OWNERS b/compose/ui/ui-test-manifest/integration-tests/testapp/OWNERS
new file mode 100644
index 0000000..42abc4e
--- /dev/null
+++ b/compose/ui/ui-test-manifest/integration-tests/testapp/OWNERS
@@ -0,0 +1,2 @@
+jellefresen@google.com
+pavlis@google.com
diff --git a/compose/ui/ui-test-manifest/integration-tests/testapp/build.gradle b/compose/ui/ui-test-manifest/integration-tests/testapp/build.gradle
new file mode 100644
index 0000000..ef1ee31
--- /dev/null
+++ b/compose/ui/ui-test-manifest/integration-tests/testapp/build.gradle
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+ id("AndroidXPlugin")
+ id("AndroidXUiPlugin")
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ kotlinPlugin(project(":compose:compiler:compiler"))
+
+ debugImplementation(project(":compose:ui:ui-test-manifest"))
+
+ androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation(ANDROIDX_TEST_RUNNER)
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 21
+ }
+}
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/VectorBenchmarkWithTracing.kt b/compose/ui/ui-test-manifest/integration-tests/testapp/src/androidTest/java/androidx/compose/ui/test/manifest/integration/testapp/ComponentActivityLaunchesTest.kt
similarity index 65%
copy from compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/VectorBenchmarkWithTracing.kt
copy to compose/ui/ui-test-manifest/integration-tests/testapp/src/androidTest/java/androidx/compose/ui/test/manifest/integration/testapp/ComponentActivityLaunchesTest.kt
index aed3bfc..a88b849 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/VectorBenchmarkWithTracing.kt
+++ b/compose/ui/ui-test-manifest/integration-tests/testapp/src/androidTest/java/androidx/compose/ui/test/manifest/integration/testapp/ComponentActivityLaunchesTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,23 +14,24 @@
* limitations under the License.
*/
-package androidx.ui.benchmark.test
+package androidx.compose.ui.test.manifest.integration.testapp
-import androidx.benchmark.macro.junit4.PerfettoRule
+import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.junit.Rule
+import org.junit.Test
import org.junit.runner.RunWith
-/**
- * Duplicate of [VectorBenchmark], but which adds tracing.
- *
- * Note: Per PerfettoRule, these benchmarks will be ignored < API 29
- */
-@Suppress("ClassName")
@LargeTest
@RunWith(AndroidJUnit4::class)
-class VectorBenchmarkWithTracing : VectorBenchmark() {
+class ComponentActivityLaunchesTest {
@get:Rule
- val perfettoRule = PerfettoRule()
+ val rule = createComposeRule()
+
+ @Test
+ fun test() {
+ rule.setContent {}
+ // Test does not crash and does not time out
+ }
}
diff --git a/compose/ui/ui-test-manifest/integration-tests/testapp/src/main/AndroidManifest.xml b/compose/ui/ui-test-manifest/integration-tests/testapp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..8f6a9da
--- /dev/null
+++ b/compose/ui/ui-test-manifest/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<!--
+ ~ 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
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.compose.ui.test.manifest.integration.testapp">
+
+ <application android:label="ui-test-manifest test app" />
+
+</manifest>
diff --git a/compose/ui/ui-test-manifest/src/main/AndroidManifest.xml b/compose/ui/ui-test-manifest/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..833d681
--- /dev/null
+++ b/compose/ui/ui-test-manifest/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.compose.ui.test.manifest">
+ <application>
+ <activity android:name="androidx.activity.ComponentActivity" />
+ </application>
+</manifest>
diff --git a/compose/ui/ui-test/lint-baseline.xml b/compose/ui/ui-test/lint-baseline.xml
index fb9c27d..75e9668 100644
--- a/compose/ui/ui-test/lint-baseline.xml
+++ b/compose/ui/ui-test/lint-baseline.xml
@@ -1,26 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
+<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
<issue
id="UnsafeNewApiCall"
@@ -29,7 +8,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/androidMain/kotlin/androidx/compose/ui/test/android/WindowCapture.android.kt"
- line="59"
+ line="64"
column="40"/>
</issue>
@@ -40,7 +19,7 @@
errorLine2=" ~~~~~~~">
<location
file="src/androidMain/kotlin/androidx/compose/ui/test/android/WindowCapture.android.kt"
- line="95"
+ line="100"
column="15"/>
</issue>
diff --git a/compose/ui/ui-text/benchmark/build.gradle b/compose/ui/ui-text/benchmark/build.gradle
index ab0fd49..beae82c 100644
--- a/compose/ui/ui-text/benchmark/build.gradle
+++ b/compose/ui/ui-text/benchmark/build.gradle
@@ -38,7 +38,7 @@
implementation(JUNIT)
androidTestImplementation project(":compose:runtime:runtime")
- androidTestImplementation project(":compose:test-utils")
+ androidTestImplementation project(":compose:benchmark-utils")
androidTestImplementation project(":compose:ui:ui")
androidTestImplementation(KOTLIN_TEST_COMMON)
androidTestImplementation(TRUTH)
diff --git a/compose/ui/ui-text/benchmark/lint-baseline.xml b/compose/ui/ui-text/benchmark/lint-baseline.xml
index b7f4e1a..5ef00a8 100644
--- a/compose/ui/ui-text/benchmark/lint-baseline.xml
+++ b/compose/ui/ui-text/benchmark/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
+<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
<issue
id="BanUncheckedReflection"
@@ -8,7 +8,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/compose/ui/text/benchmark/TextBenchmarkTestRule.kt"
- line="82"
+ line="81"
column="13"/>
</issue>
diff --git a/compose/ui/ui-text/lint-baseline.xml b/compose/ui/ui-text/lint-baseline.xml
index 48d11d8..cb16adc 100644
--- a/compose/ui/ui-text/lint-baseline.xml
+++ b/compose/ui/ui-text/lint-baseline.xml
@@ -1,104 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 28, the call containing class androidx.compose.ui.text.platform.AndroidDefaultTypeface is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
- errorLine1=" Typeface.create(Typeface.DEFAULT, fontWeight.weight, fontStyle == FontStyle.Italic)"
- errorLine2=" ~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidDefaultTypeface.android.kt"
- line="44"
- column="22"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 28, the call containing class androidx.compose.ui.text.platform.AndroidGenericFontFamilyTypeface is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
- errorLine1=" Typeface.create(nativeTypeface, fontWeight.weight, fontStyle == FontStyle.Italic)"
- errorLine2=" ~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidGenericFontFamilyTypeface.android.kt"
- line="75"
- column="22"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 28, the call containing class androidx.compose.ui.text.android.style.FontSpan is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
- errorLine1=" oldTypeface.weight"
- errorLine2=" ~~~~~~">
- <location
- file="../../../text/text/src/main/java/androidx/compose/ui/text/android/style/FontSpan.kt"
- line="46"
- column="25"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 28, the call containing class androidx.compose.ui.text.android.style.FontWeightStyleSpan is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
- errorLine1=" (weight != 0 && weight != oldTypeface?.weight)"
- errorLine2=" ~~~~~~">
- <location
- file="../../../text/text/src/main/java/androidx/compose/ui/text/android/style/FontWeightStyleSpan.kt"
- line="67"
- column="52"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 28, the call containing class androidx.compose.ui.text.android.style.FontWeightStyleSpan is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
- errorLine1=" oldTypeface?.weight ?: FontStyle.FONT_WEIGHT_NORMAL"
- errorLine2=" ~~~~~~">
- <location
- file="../../../text/text/src/main/java/androidx/compose/ui/text/android/style/FontWeightStyleSpan.kt"
- line="79"
- column="26"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 28, the call containing class androidx.compose.ui.text.android.style.FontWeightStyleSpan is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
- errorLine1=" textPaint.typeface = Typeface.create(oldTypeface, newWeight, newItalic)"
- errorLine2=" ~~~~~~">
- <location
- file="../../../text/text/src/main/java/androidx/compose/ui/text/android/style/FontWeightStyleSpan.kt"
- line="88"
- column="39"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 28, the call containing class androidx.compose.ui.text.android.style.FontWeightStyleSpan is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
- errorLine1=" textPaint.typeface = Typeface.create(oldTypeface, newWeight, newItalic)"
- errorLine2=" ~~~~~~">
- <location
- file="../../../text/text/src/main/java/androidx/compose/ui/text/android/style/FontWeightStyleSpan.kt"
- line="88"
- column="39"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 24, the call containing class androidx.compose.ui.text.platform.extensions.LocaleExtensions_androidKt is not annotated with @RequiresApi(x) where x is at least 24. Either annotate the containing class with at least @RequiresApi(24) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(24)."
- errorLine1=" android.os.LocaleList(*map { it.toJavaLocale() }.toTypedArray())"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/LocaleExtensions.android.kt"
- line="28"
- column="16"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 24, the call containing class androidx.compose.ui.text.platform.extensions.SpannableExtensions_androidKt is not annotated with @RequiresApi(x) where x is at least 24. Either annotate the containing class with at least @RequiresApi(24) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(24)."
- errorLine1=" LocaleSpan(it.toAndroidLocaleList())"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt"
- line="280"
- column="17"/>
- </issue>
+<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
<issue
id="UnsafeNewApiCall"
@@ -254,37 +155,4 @@
column="19"/>
</issue>
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 24, the call containing class androidx.compose.ui.text.platform.extensions.TextPaintExtensions_androidKt is not annotated with @RequiresApi(x) where x is at least 24. Either annotate the containing class with at least @RequiresApi(24) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(24)."
- errorLine1=" textLocales = style.localeList.toAndroidLocaleList()"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/TextPaintExtensions.android.kt"
- line="59"
- column="13"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 28, the call containing class androidx.compose.ui.text.platform.TypefaceAdapter.Companion is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
- errorLine1=" Typeface.create(typeface, finalFontWeight, finalFontStyle)"
- errorLine2=" ~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/text/platform/TypefaceAdapter.android.kt"
- line="99"
- column="26"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 28, the call containing class androidx.compose.ui.text.platform.TypefaceAdapter is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
- errorLine1=" Typeface.create("
- errorLine2=" ~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/text/platform/TypefaceAdapter.android.kt"
- line="213"
- column="22"/>
- </issue>
-
</issues>
diff --git a/compose/ui/ui-text/samples/lint-baseline.xml b/compose/ui/ui-text/samples/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/ui/ui-text/samples/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.desktop.kt b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.desktop.kt
index ecc16cb..6e2e895 100644
--- a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.desktop.kt
+++ b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.desktop.kt
@@ -62,7 +62,6 @@
import org.jetbrains.skija.paragraph.RectWidthMode
import org.jetbrains.skija.paragraph.TextBox
import java.lang.UnsupportedOperationException
-import java.nio.charset.Charset
import java.util.WeakHashMap
import kotlin.math.floor
import org.jetbrains.skija.Rect as SkRect
@@ -149,16 +148,21 @@
get() = paragraphIntrinsics.maxIntrinsicWidth
override val firstBaseline: Float
- get() = para.getLineMetrics().firstOrNull()?.run { baseline.toFloat() } ?: 0f
+ get() = lineMetrics.firstOrNull()?.run { baseline.toFloat() } ?: 0f
override val lastBaseline: Float
- get() = para.getLineMetrics().lastOrNull()?.run { baseline.toFloat() } ?: 0f
+ get() = lineMetrics.lastOrNull()?.run { baseline.toFloat() } ?: 0f
override val didExceedMaxLines: Boolean
get() = para.didExceedMaxLines()
override val lineCount: Int
- get() = para.lineNumber.toInt()
+ // workaround for https://bugs.chromium.org/p/skia/issues/detail?id=11321
+ get() = if (text == "") {
+ 1
+ } else {
+ para.lineNumber.toInt()
+ }
override val placeholderRects: List<Rect?>
get() =
@@ -188,50 +192,58 @@
Rect(box.rect.right, box.rect.top, box.rect.right + cursorWidth, box.rect.bottom)
} ?: Rect(0f, 0f, cursorWidth, paragraphIntrinsics.builder.defaultHeight)
- override fun getLineLeft(lineIndex: Int): Float {
- println("Paragraph.getLineLeft $lineIndex")
- return 0.0f
- }
+ override fun getLineLeft(lineIndex: Int): Float =
+ lineMetrics.getOrNull(lineIndex)?.left?.toFloat() ?: 0f
- override fun getLineRight(lineIndex: Int): Float {
- println("Paragraph.getLineRight $lineIndex")
- return 0.0f
- }
+ override fun getLineRight(lineIndex: Int): Float =
+ lineMetrics.getOrNull(lineIndex)?.right?.toFloat() ?: 0f
override fun getLineTop(lineIndex: Int) =
- para.lineMetrics.getOrNull(lineIndex)?.let { line ->
+ lineMetrics.getOrNull(lineIndex)?.let { line ->
floor((line.baseline - line.ascent).toFloat())
} ?: 0f
override fun getLineBottom(lineIndex: Int) =
- para.lineMetrics.getOrNull(lineIndex)?.let { line ->
+ lineMetrics.getOrNull(lineIndex)?.let { line ->
floor((line.baseline + line.descent).toFloat())
} ?: 0f
private fun lineMetricsForOffset(offset: Int): LineMetrics? {
- // For some reasons SkParagraph Line metrics use (UTF-8) byte offsets for start and end
- // indexes
- val byteOffset = text.substring(0, offset).toByteArray(Charset.forName("UTF-8")).size
- val metrics = para.lineMetrics
+ val metrics = lineMetrics
for (line in metrics) {
- if (byteOffset < line.endIndex) {
+ if (offset < line.endIndex) {
return line
}
}
+ if (metrics.isEmpty()) {
+ return null
+ }
return metrics.last()
}
- override fun getLineHeight(lineIndex: Int) = para.lineMetrics[lineIndex].height.toFloat()
+ override fun getLineHeight(lineIndex: Int) = lineMetrics[lineIndex].height.toFloat()
- override fun getLineWidth(lineIndex: Int) = para.lineMetrics[lineIndex].width.toFloat()
+ override fun getLineWidth(lineIndex: Int) = lineMetrics[lineIndex].width.toFloat()
- override fun getLineStart(lineIndex: Int) = para.lineMetrics[lineIndex].startIndex.toInt()
+ override fun getLineStart(lineIndex: Int) = lineMetrics[lineIndex].startIndex.toInt()
override fun getLineEnd(lineIndex: Int, visibleEnd: Boolean) =
if (visibleEnd) {
- para.lineMetrics[lineIndex].endExcludingWhitespaces.toInt()
+ val metrics = lineMetrics[lineIndex]
+ // workarounds for https://bugs.chromium.org/p/skia/issues/detail?id=11321 :(
+ // we are waiting for fixes
+ if (lineIndex > 0 && metrics.startIndex < lineMetrics[lineIndex - 1].endIndex) {
+ metrics.endIndex.toInt()
+ } else if (
+ metrics.startIndex < text.length &&
+ text[metrics.startIndex.toInt()] == '\n'
+ ) {
+ metrics.startIndex.toInt()
+ } else {
+ metrics.endExcludingWhitespaces.toInt()
+ }
} else {
- para.lineMetrics[lineIndex].endIndex.toInt()
+ lineMetrics[lineIndex].endIndex.toInt()
}
override fun isLineEllipsized(lineIndex: Int) = false
@@ -253,6 +265,20 @@
}
}
+ // workaround for https://bugs.chromium.org/p/skia/issues/detail?id=11321 :(
+ private val lineMetrics: Array<LineMetrics>
+ get() = if (text == "") {
+ val height = paragraphIntrinsics.builder.defaultHeight.toDouble()
+ arrayOf(
+ LineMetrics(
+ 0, 0, 0, 0, true,
+ height, 0.0, height, height, 0.0, 0.0, height, 0
+ )
+ )
+ } else {
+ para.lineMetrics
+ }
+
private fun getBoxForwardByOffset(offset: Int): TextBox? {
var to = offset + 1
while (to <= text.length) {
@@ -309,8 +335,13 @@
return box.rect.toComposeRect()
}
- override fun getWordBoundary(offset: Int) = para.getWordBoundary(offset).let {
- TextRange(it.start, it.end)
+ override fun getWordBoundary(offset: Int): TextRange {
+ if (text[offset].isWhitespace()) {
+ return TextRange(offset, offset)
+ }
+ para.getWordBoundary(offset).let {
+ return TextRange(it.start, it.end)
+ }
}
override fun paint(
diff --git a/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphTest.kt b/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphTest.kt
index d76671f..e8d8c27 100644
--- a/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphTest.kt
+++ b/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphTest.kt
@@ -92,6 +92,46 @@
}
}
+ @Test
+ fun getLineEnd() {
+ with(defaultDensity) {
+ val text = ""
+ val paragraph = simpleParagraph(
+ text = text,
+ style = TextStyle(fontSize = 50.sp)
+ )
+
+ Truth.assertThat(paragraph.getLineEnd(0, true))
+ .isEqualTo(0)
+ }
+ with(defaultDensity) {
+ val text = "ab\n\nc"
+ val paragraph = simpleParagraph(
+ text = text,
+ style = TextStyle(fontSize = 50.sp)
+ )
+
+ Truth.assertThat(paragraph.getLineEnd(0, true))
+ .isEqualTo(2)
+ Truth.assertThat(paragraph.getLineEnd(1, true))
+ .isEqualTo(3)
+ Truth.assertThat(paragraph.getLineEnd(2, true))
+ .isEqualTo(5)
+ }
+ with(defaultDensity) {
+ val text = "ab\n"
+ val paragraph = simpleParagraph(
+ text = text,
+ style = TextStyle(fontSize = 50.sp)
+ )
+
+ Truth.assertThat(paragraph.getLineEnd(0, true))
+ .isEqualTo(2)
+ Truth.assertThat(paragraph.getLineEnd(1, true))
+ .isEqualTo(3)
+ }
+ }
+
private fun simpleParagraph(
text: String = "",
style: TextStyle? = null,
diff --git a/compose/ui/ui-tooling-data/lint-baseline.xml b/compose/ui/ui-tooling-data/lint-baseline.xml
deleted file mode 100644
index 8f1aa4b..0000000
--- a/compose/ui/ui-tooling-data/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/compose/ui/ui-tooling/lint-baseline.xml b/compose/ui/ui-tooling/lint-baseline.xml
index 9d534f57..aeb18ac 100644
--- a/compose/ui/ui-tooling/lint-baseline.xml
+++ b/compose/ui/ui-tooling/lint-baseline.xml
@@ -1,26 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
+<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
<issue
id="BanUncheckedReflection"
diff --git a/compose/ui/ui-unit/lint-baseline.xml b/compose/ui/ui-unit/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/ui/ui-unit/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/ui/ui-unit/samples/lint-baseline.xml b/compose/ui/ui-unit/samples/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/ui/ui-unit/samples/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/ui/ui-util/lint-baseline.xml b/compose/ui/ui-util/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/ui/ui-util/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/ui/ui-viewbinding/lint-baseline.xml b/compose/ui/ui-viewbinding/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/ui/ui-viewbinding/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/ui/ui-viewbinding/samples/lint-baseline.xml b/compose/ui/ui-viewbinding/samples/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/ui/ui-viewbinding/samples/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/ui/ui/benchmark/build.gradle b/compose/ui/ui/benchmark/build.gradle
new file mode 100644
index 0000000..06ca891
--- /dev/null
+++ b/compose/ui/ui/benchmark/build.gradle
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.Publish
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXUiPlugin")
+ id("org.jetbrains.kotlin.android")
+ id("androidx.benchmark")
+}
+
+dependencies {
+ kotlinPlugin project(":compose:compiler:compiler")
+
+ androidTestImplementation(project(":activity:activity-compose"))
+ androidTestImplementation(project(":benchmark:benchmark-junit4"))
+ androidTestImplementation(project(":compose:foundation:foundation"))
+ androidTestImplementation(project(":compose:foundation:foundation-layout"))
+ androidTestImplementation(project(":compose:runtime:runtime"))
+ androidTestImplementation(project(":compose:benchmark-utils"))
+ androidTestImplementation(project(":compose:ui:ui"))
+ androidTestImplementation(ANDROIDX_TEST_RULES)
+ androidTestImplementation(JUNIT)
+ androidTestImplementation(KOTLIN_STDLIB)
+ androidTestImplementation(KOTLIN_TEST_COMMON)
+ androidTestImplementation(TRUTH)
+}
diff --git a/compose/ui/ui/benchmark/src/androidTest/AndroidManifest.xml b/compose/ui/ui/benchmark/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..7a944bd
--- /dev/null
+++ b/compose/ui/ui/benchmark/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="androidx.compose.ui.benchmark">
+
+ <!--
+ ~ Important: disable debuggable for accurate performance results
+ -->
+ <application
+ android:debuggable="false"
+ tools:replace="android:debuggable">
+ <!-- enable profileableByShell for non-intrusive profiling tools -->
+ <!--suppress AndroidElementNotAllowed -->
+ <profileable android:shell="true"/>
+ <activity android:name="androidx.compose.ui.input.pointer.TestActivity" />
+ </application>
+</manifest>
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/LayoutNodeModifierBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/LayoutNodeModifierBenchmark.kt
similarity index 97%
rename from compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/LayoutNodeModifierBenchmark.kt
rename to compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/LayoutNodeModifierBenchmark.kt
index 2a21cce..e93fc61 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/LayoutNodeModifierBenchmark.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/LayoutNodeModifierBenchmark.kt
@@ -16,7 +16,7 @@
@file:Suppress("DEPRECATION_ERROR")
-package androidx.ui.benchmark.test
+package androidx.compose.ui.benchmark
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -129,10 +129,9 @@
val benchmarkRule = BenchmarkRule()
override fun apply(base: Statement, description: Description?): Statement {
- return RuleChain
- .outerRule(benchmarkRule)
+ return RuleChain.outerRule(benchmarkRule)
.around(activityTestRule)
.apply(base, description)
}
}
-}
+}
\ No newline at end of file
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/OnPositionedBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/OnPositionedBenchmark.kt
similarity index 100%
rename from compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/OnPositionedBenchmark.kt
rename to compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/OnPositionedBenchmark.kt
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/autofill/AndroidAutofillBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/AndroidAutofillBenchmark.kt
similarity index 97%
rename from compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/autofill/AndroidAutofillBenchmark.kt
rename to compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/AndroidAutofillBenchmark.kt
index e234cd3..87cfc77 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/benchmark/test/autofill/AndroidAutofillBenchmark.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/AndroidAutofillBenchmark.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.ui.benchmark.test.autofill
+package androidx.compose.ui.benchmark.autofill
import android.util.SparseArray
import android.view.View
@@ -28,15 +28,15 @@
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.platform.LocalAutofillTree
import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
-import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import androidx.test.ext.junit.runners.AndroidJUnit4
@LargeTest
@OptIn(ExperimentalComposeUiApi::class)
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/AndroidTapIntegrationBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/AndroidTapIntegrationBenchmark.kt
similarity index 98%
rename from compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/AndroidTapIntegrationBenchmark.kt
rename to compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/AndroidTapIntegrationBenchmark.kt
index 4d84c53..c6c0d53 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/AndroidTapIntegrationBenchmark.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/AndroidTapIntegrationBenchmark.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.ui.pointerinput
+package androidx.compose.ui.benchmark.input.pointer
import android.content.Context
import android.view.MotionEvent
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/ComposeTapIntegrationBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/ComposeTapIntegrationBenchmark.kt
similarity index 96%
rename from compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/ComposeTapIntegrationBenchmark.kt
rename to compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/ComposeTapIntegrationBenchmark.kt
index b551802..e865c16 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/ComposeTapIntegrationBenchmark.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/ComposeTapIntegrationBenchmark.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.ui.pointerinput
+package androidx.compose.ui.benchmark.input.pointer
import android.view.View
import android.view.ViewGroup
@@ -25,7 +25,7 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.requiredHeight
-import androidx.compose.material.Text
+import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
@@ -92,7 +92,7 @@
rootView = activity.findViewById<ViewGroup>(android.R.id.content)
- activityTestRule.runOnUiThreadIR {
+ activityTestRule.runOnUiThread {
activity.setContent {
with(LocalDensity.current) {
itemHeightDp = ItemHeightPx.toDp()
@@ -174,7 +174,7 @@
@Composable
fun Email(label: String) {
- Text(
+ BasicText(
text = label,
modifier = Modifier
.pointerInput(label) {
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/TapIntegrationBenchmarkValues.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/TapIntegrationBenchmarkValues.kt
similarity index 85%
rename from compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/TapIntegrationBenchmarkValues.kt
rename to compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/TapIntegrationBenchmarkValues.kt
index 903c918..796abbc 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/TapIntegrationBenchmarkValues.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/TapIntegrationBenchmarkValues.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 20201 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,8 @@
* limitations under the License.
*/
-package androidx.ui.pointerinput
+package androidx.compose.ui.benchmark.input.pointer
val ItemHeightPx = 1f
+
val NumItems = 100
\ No newline at end of file
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/TestActivity.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/TestActivity.kt
similarity index 94%
rename from compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/TestActivity.kt
rename to compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/TestActivity.kt
index 811b1b8..e8b5f9c 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/TestActivity.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/TestActivity.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.ui.pointerinput
+package androidx.compose.ui.benchmark.input.pointer
import androidx.activity.ComponentActivity
import java.util.concurrent.CountDownLatch
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/utils.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/utils.kt
similarity index 85%
rename from compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/utils.kt
rename to compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/utils.kt
index aeb62bf..c28a69f 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/ui/pointerinput/utils.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/input/pointer/utils.kt
@@ -14,22 +14,11 @@
* limitations under the License.
*/
-package androidx.ui.pointerinput
+package androidx.compose.ui.benchmark.input.pointer
import android.view.MotionEvent
import android.view.View
-// We only need this because IR compiler doesn't like converting lambdas to Runnables
-@Suppress("DEPRECATION")
-internal fun androidx.test.rule.ActivityTestRule<*>.runOnUiThreadIR(block: () -> Unit) {
- val runnable: Runnable = object : Runnable {
- override fun run() {
- block()
- }
- }
- runOnUiThread(runnable)
-}
-
/**
* Creates a simple [MotionEvent].
*
diff --git a/compose/ui/ui/benchmark/src/main/AndroidManifest.xml b/compose/ui/ui/benchmark/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..d1cfaab
--- /dev/null
+++ b/compose/ui/ui/benchmark/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.compose.ui.benchmark">
+ <application/>
+</manifest>
diff --git a/compose/ui/ui/integration-tests/ui-demos/lint-baseline.xml b/compose/ui/ui/integration-tests/ui-demos/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/ui/ui/integration-tests/ui-demos/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/compose/ui/ui/lint-baseline.xml b/compose/ui/ui/lint-baseline.xml
index 1df8ee4..39666a2 100644
--- a/compose/ui/ui/lint-baseline.xml
+++ b/compose/ui/ui/lint-baseline.xml
@@ -1,26 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
+<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
<issue
id="BanTargetApiAnnotation"
@@ -40,7 +19,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt"
- line="719"
+ line="815"
column="13"/>
</issue>
@@ -51,161 +30,18 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt"
- line="285"
+ line="288"
column="13"/>
</issue>
<issue
id="UnsafeNewApiCall"
- message="This call is to a method from API 23, the call containing class androidx.compose.ui.autofill.AndroidAutofill_androidKt is not annotated with @RequiresApi(x) where x is at least 23. Either annotate the containing class with at least @RequiresApi(23) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(23)."
- errorLine1=" var index = root.addChildCount(autofillTree.children.count())"
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt"
- line="73"
- column="22"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 23, the call containing class androidx.compose.ui.autofill.AndroidAutofill_androidKt is not annotated with @RequiresApi(x) where x is at least 23. Either annotate the containing class with at least @RequiresApi(23) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(23)."
- errorLine1=" root.newChild(index)?.apply {"
- errorLine2=" ~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt"
- line="76"
- column="14"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 26, the call containing class androidx.compose.ui.autofill.AndroidAutofill_androidKt is not annotated with @RequiresApi(x) where x is at least 26. Either annotate the containing class with at least @RequiresApi(26) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(26)."
- errorLine1=" setAutofillId(root.autofillId!!, id)"
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt"
- line="77"
- column="13"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 26, the call containing class androidx.compose.ui.autofill.AndroidAutofill_androidKt is not annotated with @RequiresApi(x) where x is at least 26. Either annotate the containing class with at least @RequiresApi(26) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(26)."
- errorLine1=" setAutofillId(root.autofillId!!, id)"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt"
- line="77"
- column="32"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 23, the call containing class androidx.compose.ui.autofill.AndroidAutofill_androidKt is not annotated with @RequiresApi(x) where x is at least 23. Either annotate the containing class with at least @RequiresApi(23) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(23)."
- errorLine1=" setId(id, view.context.packageName, null, null)"
- errorLine2=" ~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt"
- line="78"
- column="13"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 26, the call containing class androidx.compose.ui.autofill.AndroidAutofill_androidKt is not annotated with @RequiresApi(x) where x is at least 26. Either annotate the containing class with at least @RequiresApi(26) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(26)."
- errorLine1=" setAutofillType(View.AUTOFILL_TYPE_TEXT)"
- errorLine2=" ~~~~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt"
- line="79"
- column="13"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 26, the call containing class androidx.compose.ui.autofill.AndroidAutofill_androidKt is not annotated with @RequiresApi(x) where x is at least 26. Either annotate the containing class with at least @RequiresApi(26) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(26)."
- errorLine1=" setAutofillHints(autofillNode.autofillTypes.map { it.androidType }.toTypedArray())"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt"
- line="80"
- column="13"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 23, the call containing class androidx.compose.ui.autofill.AndroidAutofill_androidKt is not annotated with @RequiresApi(x) where x is at least 23. Either annotate the containing class with at least @RequiresApi(23) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(23)."
- errorLine1=" setDimens(left, top, 0, 0, width(), height())"
- errorLine2=" ~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt"
- line="92"
- column="17"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 26, the call containing class androidx.compose.ui.autofill.AndroidAutofill_androidKt is not annotated with @RequiresApi(x) where x is at least 26. Either annotate the containing class with at least @RequiresApi(26) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(26)."
- errorLine1=" value.isText -> autofillTree.performAutofill(itemId, value.textValue.toString())"
- errorLine2=" ~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt"
- line="109"
- column="19"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 26, the call containing class androidx.compose.ui.autofill.AndroidAutofill_androidKt is not annotated with @RequiresApi(x) where x is at least 26. Either annotate the containing class with at least @RequiresApi(26) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(26)."
- errorLine1=" value.isText -> autofillTree.performAutofill(itemId, value.textValue.toString())"
- errorLine2=" ~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt"
- line="109"
- column="72"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 26, the call containing class androidx.compose.ui.autofill.AndroidAutofill_androidKt is not annotated with @RequiresApi(x) where x is at least 26. Either annotate the containing class with at least @RequiresApi(26) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(26)."
- errorLine1=" value.isDate -> TODO("b/138604541: Add onFill() callback for date")"
- errorLine2=" ~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt"
- line="110"
- column="19"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 26, the call containing class androidx.compose.ui.autofill.AndroidAutofill_androidKt is not annotated with @RequiresApi(x) where x is at least 26. Either annotate the containing class with at least @RequiresApi(26) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(26)."
- errorLine1=" value.isList -> TODO("b/138604541: Add onFill() callback for list")"
- errorLine2=" ~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt"
- line="111"
- column="19"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 26, the call containing class androidx.compose.ui.autofill.AndroidAutofill_androidKt is not annotated with @RequiresApi(x) where x is at least 26. Either annotate the containing class with at least @RequiresApi(26) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(26)."
- errorLine1=" value.isToggle -> TODO("b/138604541: Add onFill() callback for toggle")"
- errorLine2=" ~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofill.android.kt"
- line="112"
- column="19"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
message="This call is to a method from API 26, the call containing class androidx.compose.ui.autofill.AndroidAutofillDebugUtils_androidKt is not annotated with @RequiresApi(x) where x is at least 26. Either annotate the containing class with at least @RequiresApi(26) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(26)."
errorLine1=" autofillManager.registerCallback(AutofillCallback)"
errorLine2=" ~~~~~~~~~~~~~~~~">
<location
file="src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillDebugUtils.android.kt"
- line="65"
+ line="67"
column="21"/>
</issue>
@@ -216,56 +52,12 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~">
<location
file="src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillDebugUtils.android.kt"
- line="74"
+ line="77"
column="21"/>
</issue>
<issue
id="UnsafeNewApiCall"
- message="This call is to a method from API 26, the call containing class androidx.compose.ui.platform.AndroidComposeView is not annotated with @RequiresApi(x) where x is at least 26. Either annotate the containing class with at least @RequiresApi(26) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(26)."
- errorLine1=" focusable = View.FOCUSABLE"
- errorLine2=" ~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt"
- line="280"
- column="13"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 26, the call containing class androidx.compose.ui.platform.AndroidComposeView is not annotated with @RequiresApi(x) where x is at least 26. Either annotate the containing class with at least @RequiresApi(26) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(26)."
- errorLine1=" defaultFocusHighlightEnabled = false"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt"
- line="282"
- column="13"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 26, the call containing class androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat is not annotated with @RequiresApi(x) where x is at least 26. Either annotate the containing class with at least @RequiresApi(26) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(26)."
- errorLine1=" info.unwrap().availableExtraData = listOf(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY)"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt"
- line="407"
- column="27"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 23, the call containing class androidx.compose.ui.platform.AndroidTextToolbar is not annotated with @RequiresApi(x) where x is at least 23. Either annotate the containing class with at least @RequiresApi(23) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(23)."
- errorLine1=" actionMode = view.startActionMode("
- errorLine2=" ~~~~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/platform/AndroidTextToolbar.android.kt"
- line="53"
- column="31"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
message="This call is to a method from API 23, the call containing class androidx.compose.ui.res.ColorResources_androidKt is not annotated with @RequiresApi(x) where x is at least 23. Either annotate the containing class with at least @RequiresApi(23) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(23)."
errorLine1=" Color(context.resources.getColor(id, context.theme))"
errorLine2=" ~~~~~~~~">
@@ -286,26 +78,4 @@
column="5"/>
</issue>
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 26, the call containing class androidx.compose.ui.platform.RenderNodeLayer is not annotated with @RequiresApi(x) where x is at least 26. Either annotate the containing class with at least @RequiresApi(26) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(26)."
- errorLine1=" ownerView.parent?.onDescendantInvalidated(ownerView, ownerView)"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt"
- line="165"
- column="31"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 29, the call containing class androidx.compose.ui.platform.Wrapper_androidKt 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=" owner.attributeSourceResourceMap.isNotEmpty()"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/androidMain/kotlin/androidx/compose/ui/platform/Wrapper.android.kt"
- line="308"
- column="15"/>
- </issue>
-
</issues>
diff --git a/compose/ui/ui/samples/lint-baseline.xml b/compose/ui/ui/samples/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/compose/ui/ui/samples/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
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 d677d07..00c1896 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
@@ -32,6 +32,7 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.requiredSize
@@ -47,9 +48,12 @@
import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.paneTitle
+import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.textSelectionRange
import androidx.compose.ui.test.SemanticsMatcher
@@ -416,7 +420,7 @@
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
- fun testAddExtraDataToAccessibilityNodeInfo() {
+ fun testAddExtraDataToAccessibilityNodeInfo_notMerged() {
val tag = "TextField"
lateinit var textLayoutResult: TextLayoutResult
@@ -454,6 +458,48 @@
assertEquals(expectedRect.bottom, rectF.bottom)
}
+ // This test needs to be improved after text merging(b/157474582) is fixed.
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ fun testAddExtraDataToAccessibilityNodeInfo_merged() {
+ val tag = "MergedText"
+ val textOne = "hello"
+ val textTwo = "world"
+ lateinit var textLayoutResult: TextLayoutResult
+
+ container.setContent {
+ Column(modifier = Modifier.testTag(tag).semantics(true) {}) {
+ BasicText(text = textOne, onTextLayout = { textLayoutResult = it })
+ BasicText(text = textTwo)
+ }
+ }
+
+ val textNode = rule.onNodeWithTag(tag)
+ .fetchSemanticsNode("couldn't find node with tag $tag")
+ val info = AccessibilityNodeInfo.obtain()
+ val argument = Bundle()
+ val length = textOne.length + textTwo.length
+ argument.putInt(AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX, 0)
+ argument.putInt(AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH, length)
+ provider.addExtraDataToAccessibilityNodeInfo(
+ textNode.id,
+ info,
+ AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY,
+ argument
+ )
+ val data = info.extras
+ .getParcelableArray(AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY)
+ assertEquals(length, data!!.size)
+ val rectF = data[0] as RectF
+ val expectedRect = textLayoutResult.getBoundingBox(0).translate(
+ textNode.positionInWindow
+ )
+ assertEquals(expectedRect.left, rectF.left)
+ assertEquals(expectedRect.top, rectF.top)
+ assertEquals(expectedRect.right, rectF.right)
+ assertEquals(expectedRect.bottom, rectF.bottom)
+ }
+
@Test
fun sendStateChangeEvent_whenClickToggleable() {
val tag = "Toggleable"
@@ -1132,6 +1178,121 @@
}
}
+ @Test
+ fun testContentDescription_notMergingDescendants_withOwnContentDescription() {
+ val tag = "Column"
+ container.setContent {
+ Column(Modifier.semantics { contentDescription = "Column" }.testTag(tag)) {
+ BasicText("Text")
+ Box(Modifier.size(100.dp).semantics { contentDescription = "Box" })
+ }
+ }
+
+ val node = rule.onNodeWithTag(tag).fetchSemanticsNode()
+ val info = provider.createAccessibilityNodeInfo(node.id)
+
+ assertEquals("Column", info.contentDescription)
+ }
+
+ @Test
+ fun testContentDescription_mergingDescendants_withOwnContentDescription() {
+ val tag = "Column"
+ container.setContent {
+ Column(Modifier.semantics(true) { contentDescription = "Column" }.testTag(tag)) {
+ BasicText("Text")
+ Box(Modifier.size(100.dp).semantics { contentDescription = "Box" })
+ }
+ }
+
+ val node = rule.onNodeWithTag(tag).fetchSemanticsNode()
+ val info = provider.createAccessibilityNodeInfo(node.id)
+
+ assertEquals("Column", info.contentDescription)
+ }
+
+ @Test
+ fun testContentDescription_notMergingDescendants_withoutOwnContentDescription() {
+ val tag = "Column"
+ container.setContent {
+ Column(Modifier.semantics {}.testTag(tag)) {
+ BasicText("Text")
+ Box(Modifier.size(100.dp).semantics { contentDescription = "Box" })
+ }
+ }
+
+ val node = rule.onNodeWithTag(tag).fetchSemanticsNode()
+ val info = provider.createAccessibilityNodeInfo(node.id)
+
+ assertEquals(null, info.contentDescription)
+ }
+
+ @Test
+ fun testContentDescription_mergingDescendants_withoutOwnContentDescription() {
+ val tag = "Column"
+ container.setContent {
+ Column(Modifier.semantics(true) {}.testTag(tag)) {
+ BasicText("Text")
+ Box(Modifier.size(100.dp).semantics { contentDescription = "Box" })
+ }
+ }
+
+ val node = rule.onNodeWithTag(tag).fetchSemanticsNode()
+ val info = provider.createAccessibilityNodeInfo(node.id)
+
+ assertEquals("Text, Box", info.contentDescription)
+ }
+
+ @Test
+ fun testContentDescription_mergingDescendants() {
+ // This is a bit more complex example
+ val tag = "Column"
+ container.setContent {
+ Column(Modifier.semantics(true) {}.testTag(tag)) {
+ Column(Modifier.semantics(true) { contentDescription = "Column1" }) {
+ BasicText("Text1")
+ Row(Modifier.semantics {}) {
+ Box(Modifier.size(100.dp).semantics { contentDescription = "Box1" })
+ Box(Modifier.size(100.dp).semantics { contentDescription = "Box2" })
+ }
+ }
+ Column(Modifier.semantics {}) {
+ BasicText("Text2")
+ Row(Modifier.semantics(true) {}) {
+ Box(Modifier.size(100.dp).semantics { contentDescription = "Box3" })
+ Box(Modifier.size(100.dp).semantics { contentDescription = "Box4" })
+ }
+ }
+ Column(Modifier.semantics { }) {
+ BasicText("Text3")
+ Row(Modifier.semantics {}) {
+ Box(Modifier.size(100.dp).semantics { contentDescription = "Box5" })
+ Box(Modifier.size(100.dp).semantics { contentDescription = "Box6" })
+ }
+ }
+ }
+ }
+
+ val node = rule.onNodeWithTag(tag).fetchSemanticsNode()
+ val info = provider.createAccessibilityNodeInfo(node.id)
+
+ assertEquals("Text2, Text3, Box5, Box6", info.contentDescription)
+ }
+
+ @Test
+ fun testRole_doesNotMerge() {
+ container.setContent {
+ Row(Modifier.semantics(true) {}.testTag("Row")) {
+ Box(Modifier.size(100.dp).semantics { role = Role.Button })
+ Box(Modifier.size(100.dp).semantics { role = Role.Image })
+ }
+ }
+
+ val node = rule.onNodeWithTag("Row").fetchSemanticsNode()
+ val info = provider.createAccessibilityNodeInfo(node.id)
+
+ assertEquals(AndroidComposeViewAccessibilityDelegateCompat.ClassName, info.className)
+ }
+
private fun eventIndex(list: List<AccessibilityEvent>, event: AccessibilityEvent): Int {
for (i in list.indices) {
if (ReflectionEquals(list[i], null).matches(event)) {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
index 7aa4733..dbd0c19 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
@@ -19,11 +19,15 @@
import android.os.Build
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.testutils.assertPixels
import androidx.compose.ui.AtLeastSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
@@ -32,6 +36,7 @@
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.LocalLayoutDirection
@@ -43,6 +48,7 @@
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
@@ -343,6 +349,63 @@
}
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun testGraphicsLayerCacheInvalidatedAfterStateChange() {
+ // Verify that a state change within the cache block does
+ // require the cache block to be invalidated if a graphicsLayer is also
+ // configured on the composable and the state parameter is configured elsewhere
+ val boxTag = "boxTag"
+ val clickTag = "clickTag"
+
+ var cacheBuildCount = 0
+
+ rule.setContent {
+ val flag = remember { mutableStateOf(false) }
+ Column {
+ AtLeastSize(
+ size = 50,
+ modifier = Modifier.testTag(boxTag)
+ .graphicsLayer()
+ .drawWithCache {
+ // State read of flag
+ val color = if (flag.value) Color.Red else Color.Blue
+ cacheBuildCount++
+
+ onDrawBehind {
+ drawRect(color)
+ }
+ }
+ )
+
+ Box(
+ Modifier.testTag(clickTag)
+ .size(20.dp)
+ .clickable {
+ flag.value = !flag.value
+ }
+ )
+ }
+ }
+
+ rule.onNodeWithTag(boxTag).apply {
+ // Verify that the cache lambda was invoked once
+ assertEquals(1, cacheBuildCount)
+ captureToImage().assertPixels { Color.Blue }
+ }
+
+ rule.onNodeWithTag(clickTag).performClick()
+
+ rule.waitForIdle()
+
+ rule.onNodeWithTag(boxTag).apply {
+ // Verify the cache lambda was invoked again and the
+ // rect is drawn with the updated color
+ assertEquals(2, cacheBuildCount)
+ captureToImage().assertPixels { Color.Red }
+ }
+ }
+
// Helper Modifier that uses Modifier.drawWithCache internally. If the color
// parameter
private fun Modifier.drawPathHelperModifier(color: Color) =
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
index 841ca95..0f6d405 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
@@ -18,12 +18,16 @@
import android.graphics.Bitmap
import android.os.Build
+import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.requiredWidthIn
import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.testutils.assertPixels
@@ -49,6 +53,7 @@
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.vector.Path
@@ -344,6 +349,23 @@
}
}
+ @Test
+ fun testUnboundedPainterDoesNotCrash() {
+ rule.setContent {
+ LazyColumn(Modifier.fillMaxSize().padding(16.dp)) {
+ item {
+ // Lazy column has unbounded height so ensure that the constraints
+ // provided to Painters without an intrinsic size are with a finite
+ // range (i.e. don't crash)
+ Image(
+ painter = ColorPainter(Color.Black),
+ contentDescription = ""
+ )
+ }
+ }
+ }
+ }
+
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun testPainterNotSizedToIntrinsics() {
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 479ceb7..3c13021 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
@@ -143,6 +143,27 @@
}
@Test
+ fun nestedMergedSubtree_includeAllMergeableChildren() {
+ val tag1 = "tag1"
+ val tag2 = "tag2"
+ val label1 = "foo"
+ val label2 = "bar"
+ val label3 = "hi"
+ rule.setContent {
+ SimpleTestLayout(Modifier.semantics(mergeDescendants = true) {}.testTag(tag1)) {
+ SimpleTestLayout(Modifier.semantics { contentDescription = label1 }) { }
+ SimpleTestLayout(Modifier.semantics(mergeDescendants = true) {}.testTag(tag2)) {
+ SimpleTestLayout(Modifier.semantics { contentDescription = label2 }) { }
+ }
+ SimpleTestLayout(Modifier.semantics { contentDescription = label3 }) { }
+ }
+ }
+
+ rule.onNodeWithTag(tag1).assertContentDescriptionEquals("$label1, $label3")
+ rule.onNodeWithTag(tag2).assertContentDescriptionEquals(label2)
+ }
+
+ @Test
fun clearAndSetSemantics() {
val tag1 = "tag1"
val tag2 = "tag2"
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 91251f3..2854452 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -310,8 +310,11 @@
setText(semanticsNode, info)
info.stateDescription =
semanticsNode.config.getOrNull(SemanticsProperties.StateDescription)
- info.contentDescription =
- semanticsNode.config.getOrNull(SemanticsProperties.ContentDescription)
+
+ // If the node has a content description (in unmerged config), it will be used. Otherwise
+ // for merging node we concatenate content descriptions and texts from its children.
+ info.contentDescription = calculateContentDescription(semanticsNode)
+
semanticsNode.config.getOrNull(SemanticsProperties.Heading)?.let {
info.isHeading = true
}
@@ -420,7 +423,10 @@
AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_WORD or
AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_PARAGRAPH
// We only traverse the text when contentDescription is not set.
- if (info.contentDescription.isNullOrEmpty() &&
+ val contentDescription = semanticsNode.unmergedConfig.getOrNull(
+ SemanticsProperties.ContentDescription
+ )
+ if (contentDescription.isNullOrEmpty() &&
semanticsNode.config.contains(SemanticsActions.GetTextLayoutResult)
) {
info.movementGranularities = info.movementGranularities or
@@ -1093,7 +1099,7 @@
return
}
val textLayoutResults = mutableListOf<TextLayoutResult>()
- // Note now it only works for single Text/TextField until we fix the merging issue.
+ // Note now it only works for single Text/TextField until we fix b/157474582.
val getLayoutResult = node.config[SemanticsActions.GetTextLayoutResult]
.action?.invoke(textLayoutResults)
val textLayoutResult: TextLayoutResult
@@ -1105,6 +1111,11 @@
val boundingRects = mutableListOf<RectF?>()
val textNode: SemanticsNode? = node.findNonEmptyTextChild()
for (i in 0 until positionInfoLength) {
+ // This is a workaround until we fix the merging issue in b/157474582.
+ if (positionInfoStartIndex + i >= textLayoutResult.layoutInput.text.length) {
+ boundingRects.add(null)
+ continue
+ }
val bounds = textLayoutResult.getBoundingBox(positionInfoStartIndex + i)
val screenBounds: Rect?
// Only the visible/partial visible locations are used.
@@ -1681,6 +1692,7 @@
extendSelection: Boolean
): Boolean {
val text = getIterableTextForAccessibility(node)
+ ?: calculateContentDescriptionFromChildren(node)
if (text.isNullOrEmpty()) {
return false
}
@@ -1729,7 +1741,9 @@
event.toIndex = toIndex
event.action = action
event.movementGranularity = granularity
- event.text.add(getIterableTextForAccessibility(node))
+ event.text.add(
+ getIterableTextForAccessibility(node) ?: calculateContentDescriptionFromChildren(node)
+ )
sendEvent(event)
}
@@ -1771,7 +1785,7 @@
private fun getAccessibilitySelectionStart(node: SemanticsNode): Int {
// If there is ContentDescription, it will be used instead of text during traversal.
- if (!node.config.contains(SemanticsProperties.ContentDescription) &&
+ if (!node.unmergedConfig.contains(SemanticsProperties.ContentDescription) &&
node.config.contains(SemanticsProperties.TextSelectionRange)
) {
return node.config[SemanticsProperties.TextSelectionRange].start
@@ -1781,7 +1795,7 @@
private fun getAccessibilitySelectionEnd(node: SemanticsNode): Int {
// If there is ContentDescription, it will be used instead of text during traversal.
- if (!node.config.contains(SemanticsProperties.ContentDescription) &&
+ if (!node.unmergedConfig.contains(SemanticsProperties.ContentDescription) &&
node.config.contains(SemanticsProperties.TextSelectionRange)
) {
return node.config[SemanticsProperties.TextSelectionRange].end
@@ -1799,7 +1813,10 @@
node: SemanticsNode?,
granularity: Int
): AccessibilityIterators.TextSegmentIterator? {
+ if (node == null) return null
+
val text = getIterableTextForAccessibility(node)
+ ?: calculateContentDescriptionFromChildren(node)
if (text.isNullOrEmpty()) {
return null
}
@@ -1826,7 +1843,7 @@
AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_LINE,
AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_PAGE -> {
// Line and page granularity are only for static text or text field.
- if (node == null || !node.config.contains(SemanticsActions.GetTextLayoutResult)) {
+ if (!node.config.contains(SemanticsActions.GetTextLayoutResult)) {
return null
}
// TODO(b/157474582): Note now it only works for single Text/TextField until we
@@ -1855,9 +1872,11 @@
}
/**
- * Gets the text reported for accessibility purposes.
+ * Gets the text reported for accessibility purposes. If a text node has a content description
+ * in the unmerged config, it will be used instead of the text.
*
- * @return The accessibility text.
+ * This function is basically prioritising the content description over the text or editable
+ * text of the text and text field nodes.
*/
private fun getIterableTextForAccessibility(node: SemanticsNode?): String? {
if (node == null) {
@@ -1865,8 +1884,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.ContentDescription)) {
- return node.config[SemanticsProperties.ContentDescription]
+ if (node.unmergedConfig.contains(SemanticsProperties.ContentDescription)) {
+ return node.unmergedConfig[SemanticsProperties.ContentDescription]
}
if (node.isTextField) {
@@ -1891,6 +1910,85 @@
}
}
+ /**
+ * Content description of the node that itself has a content description will be reported as
+ * is. Text node and text field node without a content description ignore it and report null.
+ * In other situations we concatenate non-merging children's content description or texts
+ * using [calculateContentDescriptionFromChildren]. Note that we ignore merging children as
+ * they should be focused separately.
+ *
+ * This method is used to set the content description of the node.
+ */
+ private fun calculateContentDescription(node: SemanticsNode): String? {
+ val contentDescription =
+ node.unmergedConfig.getOrNull(SemanticsProperties.ContentDescription)
+ if (!contentDescription.isNullOrEmpty()) {
+ return contentDescription
+ }
+
+ if (node.unmergedConfig.contains(SemanticsProperties.Text) ||
+ node.unmergedConfig.contains(SemanticsActions.SetText)
+ ) {
+ return null
+ }
+
+ // if node merges its children, concatenate their content descriptions and texts
+ return calculateContentDescriptionFromChildren(node)
+ }
+
+ /**
+ * Concatenate content descriptions and texts of non-merging children of the [node] that
+ * merges its children.
+ */
+ fun calculateContentDescriptionFromChildren(node: SemanticsNode): String? {
+ fun concatenateChildrenContentDescriptionAndText(node: SemanticsNode): List<String> {
+ val childDescriptions = mutableListOf<String>()
+
+ node.unmergedChildren().fastForEach { childNode ->
+ // Don't merge child that merges its children because that child node will be focused
+ // separately
+ if (childNode.unmergedConfig.isMergingSemanticsOfDescendants) {
+ return@fastForEach
+ }
+
+ val contentDescription =
+ childNode.unmergedConfig.getOrNull(SemanticsProperties.ContentDescription)
+ if (!contentDescription.isNullOrEmpty()) {
+ childDescriptions.add(contentDescription)
+ return@fastForEach
+ }
+
+ // check if it's a text field node
+ if (childNode.config.contains(SemanticsActions.SetText)) {
+ val text = getTextForTextField(childNode)
+ if (!text.isNullOrEmpty()) {
+ childDescriptions.add(text)
+ }
+ return@fastForEach
+ }
+
+ // check if it's a text node
+ val text = childNode.unmergedConfig.getOrNull(SemanticsProperties.Text)
+ if (!text.isNullOrEmpty()) {
+ childDescriptions.add(text.text)
+ return@fastForEach
+ }
+
+ concatenateChildrenContentDescriptionAndText(childNode).fastForEach {
+ childDescriptions.add(it)
+ }
+ }
+
+ return childDescriptions
+ }
+
+ // if node merges its children, concatenate their content descriptions and texts
+ if (node.unmergedConfig.isMergingSemanticsOfDescendants) {
+ return concatenateChildrenContentDescriptionAndText(node).joinToString()
+ }
+ return null
+ }
+
private fun setCollectionInfo(node: SemanticsNode, info: AccessibilityNodeInfoCompat) {
val groupedChildren = mutableListOf<SemanticsNode>()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
index 57d134e..f76a069 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
@@ -199,7 +199,9 @@
}
private fun modifyConstraints(constraints: Constraints): Constraints {
- if (!useIntrinsicSize || (constraints.hasFixedWidth && constraints.hasFixedHeight)) {
+ val hasBoundedDimens = constraints.hasBoundedWidth && constraints.hasBoundedHeight
+ val hasFixedDimens = constraints.hasFixedWidth && constraints.hasFixedHeight
+ if ((!useIntrinsicSize && hasBoundedDimens) || hasFixedDimens) {
// If we have fixed constraints or we are not attempting to size the
// composable based on the size of the Painter, do not attempt to
// modify them. Otherwise rely on Alignment and ContentScale
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
index 2897c48..ebbc0b2 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
@@ -165,7 +165,7 @@
* have the same [zIndex] the order in which the items were placed is used.
*/
fun Placeable.placeRelative(x: Int, y: Int, zIndex: Float = 0f) =
- placeRelative(IntOffset(x, y), zIndex)
+ placeAutoMirrored(IntOffset(x, y), zIndex, null)
/**
* Place a [Placeable] at [x], [y] in its parent's coordinate system.
@@ -176,7 +176,8 @@
* [zIndex] will be drawn on top of all the children with smaller [zIndex]. When children
* have the same [zIndex] the order in which the items were placed is used.
*/
- fun Placeable.place(x: Int, y: Int, zIndex: Float = 0f) = place(IntOffset(x, y), zIndex)
+ fun Placeable.place(x: Int, y: Int, zIndex: Float = 0f) =
+ placeApparentToRealOffset(IntOffset(x, y), zIndex, null)
/**
* Place a [Placeable] at [position] in its parent's coordinate system.
@@ -235,7 +236,7 @@
y: Int,
zIndex: Float = 0f,
layerBlock: GraphicsLayerScope.() -> Unit = DefaultLayerBlock
- ) = placeRelativeWithLayer(IntOffset(x, y), zIndex, layerBlock)
+ ) = placeAutoMirrored(IntOffset(x, y), zIndex, layerBlock)
/**
* Place a [Placeable] at [x], [y] in its parent's coordinate system with an introduced
@@ -255,7 +256,7 @@
y: Int,
zIndex: Float = 0f,
layerBlock: GraphicsLayerScope.() -> Unit = DefaultLayerBlock
- ) = placeWithLayer(IntOffset(x, y), zIndex, layerBlock)
+ ) = placeApparentToRealOffset(IntOffset(x, y), zIndex, layerBlock)
/**
* Place a [Placeable] at [position] in its parent's coordinate system with an introduced
@@ -276,10 +277,11 @@
layerBlock: GraphicsLayerScope.() -> Unit = DefaultLayerBlock
) = placeApparentToRealOffset(position, zIndex, layerBlock)
- private fun Placeable.placeAutoMirrored(
+ @Suppress("NOTHING_TO_INLINE")
+ internal inline fun Placeable.placeAutoMirrored(
position: IntOffset,
zIndex: Float,
- layerBlock: (GraphicsLayerScope.() -> Unit)?
+ noinline layerBlock: (GraphicsLayerScope.() -> Unit)?
) {
if (parentLayoutDirection == LayoutDirection.Ltr || parentWidth == 0) {
placeApparentToRealOffset(position, zIndex, layerBlock)
@@ -292,10 +294,11 @@
}
}
- private fun Placeable.placeApparentToRealOffset(
+ @Suppress("NOTHING_TO_INLINE")
+ internal inline fun Placeable.placeApparentToRealOffset(
position: IntOffset,
zIndex: Float,
- layerBlock: (GraphicsLayerScope.() -> Unit)?
+ noinline layerBlock: (GraphicsLayerScope.() -> Unit)?
) {
placeAt(position + apparentToRealOffset, zIndex, layerBlock)
}
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 63529ac..9e961e0 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
@@ -104,7 +104,7 @@
}
}
- override fun performMeasure(constraints: Constraints): Placeable {
+ override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
val placeable = wrapped.measure(constraints)
measureResult = object : MeasureResult {
override val width: Int = wrapped.measureResult.width
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 0093059..1ecd191 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
@@ -40,7 +40,7 @@
override val measureScope get() = layoutNode.measureScope
- override fun performMeasure(constraints: Constraints): Placeable {
+ override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
val measureResult = with(layoutNode.measurePolicy) {
layoutNode.measureScope.measure(layoutNode.children, constraints)
}
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 844d0a6..f804685 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
@@ -50,7 +50,7 @@
internal abstract class LayoutNodeWrapper(
internal val layoutNode: LayoutNode
) : Placeable(), Measurable, LayoutCoordinates, OwnerScope, (Canvas) -> Unit {
- internal open val wrapped: LayoutNodeWrapper? = null
+ internal open val wrapped: LayoutNodeWrapper? get() = null
internal var wrappedBy: LayoutNodeWrapper? = null
/**
@@ -69,7 +69,7 @@
private set
private var _isAttached = false
- override val isAttached: Boolean
+ final override val isAttached: Boolean
get() {
if (_isAttached) {
require(layoutNode.isAttached)
@@ -78,35 +78,46 @@
}
private var _measureResult: MeasureResult? = null
- open var measureResult: MeasureResult
+ var measureResult: MeasureResult
get() = _measureResult ?: error(UnmeasuredError)
internal set(value) {
- if (value.width != _measureResult?.width || value.height != _measureResult?.height) {
- val layer = layer
- if (layer != null) {
- layer.resize(IntSize(value.width, value.height))
- } else {
- wrappedBy?.invalidateLayer()
+ val old = _measureResult
+ if (value !== old) {
+ _measureResult = value
+ if (old == null || value.width != old.width || value.height != old.height) {
+ onMeasureResultChanged(value.width, value.height)
}
- layoutNode.owner?.onLayoutChange(layoutNode)
}
- _measureResult = value
- measuredSize = IntSize(measureResult.width, measureResult.height)
}
+ /**
+ * Called when the width or height of [measureResult] change. The object instance pointed to
+ * by [measureResult] may or may not have changed.
+ */
+ protected open fun onMeasureResultChanged(width: Int, height: Int) {
+ val layer = layer
+ if (layer != null) {
+ layer.resize(IntSize(width, height))
+ } else {
+ wrappedBy?.invalidateLayer()
+ }
+ layoutNode.owner?.onLayoutChange(layoutNode)
+ measuredSize = IntSize(width, height)
+ }
+
var position: IntOffset = IntOffset.Zero
private set
var zIndex: Float = 0f
protected set
- override val parentLayoutCoordinates: LayoutCoordinates?
+ final override val parentLayoutCoordinates: LayoutCoordinates?
get() {
check(isAttached) { ExpectAttachedLayoutCoordinates }
return layoutNode.outerLayoutNodeWrapper.wrappedBy
}
- override val parentCoordinates: LayoutCoordinates?
+ final override val parentCoordinates: LayoutCoordinates?
get() {
check(isAttached) { ExpectAttachedLayoutCoordinates }
return wrappedBy?.getWrappedByCoordinates()
@@ -143,17 +154,12 @@
return x >= 0f && y >= 0f && x < measuredWidth && y < measuredHeight
}
- /**
- * Measures the modified child.
- */
- abstract fun performMeasure(constraints: Constraints): Placeable
-
- /**
- * Measures the modified child.
- */
- final override fun measure(constraints: Constraints): Placeable {
+ protected inline fun performingMeasure(
+ constraints: Constraints,
+ block: () -> Placeable
+ ): Placeable {
measurementConstraints = constraints
- val result = performMeasure(constraints)
+ val result = block()
layer?.resize(measuredSize)
return result
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedDrawNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedDrawNode.kt
index 3f58143..cca6cd7 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedDrawNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedDrawNode.kt
@@ -21,7 +21,6 @@
import androidx.compose.ui.draw.DrawModifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Canvas
-import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.toSize
@@ -80,16 +79,10 @@
invalidateCache = true
}
- override var measureResult: MeasureResult
- get() = super.measureResult
- set(value) {
- if (super.measuredSize.width != value.width ||
- super.measuredSize.height != value.height
- ) {
- invalidateCache = true
- }
- super.measureResult = value
- }
+ override fun onMeasureResultChanged(width: Int, height: Int) {
+ super.onMeasureResultChanged(width, height)
+ invalidateCache = true
+ }
// This is not thread safe
override fun performDraw(canvas: Canvas) {
@@ -119,10 +112,8 @@
private val onCommitAffectingModifiedDrawNode: (ModifiedDrawNode) -> Unit =
{ modifiedDrawNode ->
if (modifiedDrawNode.isValid) {
- // Note this intentionally does not invalidate the layer as Owner implementations
- // already observe and invalidate the layer on state changes. Instead just
- // mark the cache dirty so that it will be re-created on the next draw
modifiedDrawNode.invalidateCache = true
+ modifiedDrawNode.invalidateLayer()
}
}
}
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 ec54573..614d548 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
@@ -32,9 +32,11 @@
modifier: LayoutModifier
) : DelegatingLayoutNodeWrapper<LayoutModifier>(wrapped, modifier) {
- override fun performMeasure(constraints: Constraints): Placeable = with(modifier) {
- measureResult = measureScope.measure(wrapped, constraints)
- this@ModifiedLayoutNode
+ override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
+ with(modifier) {
+ measureResult = measureScope.measure(wrapped, constraints)
+ this@ModifiedLayoutNode
+ }
}
override fun minIntrinsicWidth(height: Int): Int =
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/RemeasureModifierWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/RemeasureModifierWrapper.kt
index f37eb1b..fdb9840 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/RemeasureModifierWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/RemeasureModifierWrapper.kt
@@ -27,8 +27,8 @@
wrapped: LayoutNodeWrapper,
modifier: OnRemeasuredModifier
) : DelegatingLayoutNodeWrapper<OnRemeasuredModifier>(wrapped, modifier) {
- override fun performMeasure(constraints: Constraints): Placeable {
- val placeable = super.performMeasure(constraints)
+ override fun measure(constraints: Constraints): Placeable {
+ val placeable = super.measure(constraints)
val invokeRemeasureCallbacks = {
modifier.onRemeasured(measuredSize)
}
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 e4836c2..ffac306 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
@@ -163,12 +163,10 @@
unmergedChildren().fastForEach { child ->
// Don't merge children that themselves merge all their descendants (because that
// indicates they're independently screen-reader-focusable).
- if (child.isMergingSemanticsOfDescendants) {
- return
+ if (!child.isMergingSemanticsOfDescendants) {
+ mergedConfig.mergeChild(child.unmergedConfig)
+ child.mergeConfig(mergedConfig)
}
-
- mergedConfig.mergeChild(child.unmergedConfig)
- child.mergeConfig(mergedConfig)
}
}
}
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 bc63852..3e404c8 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
@@ -148,7 +148,7 @@
*
* @see SemanticsPropertyReceiver.role
*/
- val Role = SemanticsPropertyKey<Role>("Role")
+ val Role = SemanticsPropertyKey<Role>("Role") { parentValue, _ -> parentValue }
/**
* @see SemanticsPropertyReceiver.testTag
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.desktop.kt
index 130a434..6f3e90e 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.desktop.kt
@@ -188,6 +188,7 @@
)
}
}
+ wrapped.focusTraversalKeysEnabled = false
wrapped.addKeyListener(object : KeyAdapter() {
override fun keyPressed(event: KeyEvent) = events.post {
owners.onKeyPressed(event)
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/key/Key.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/key/Key.desktop.kt
index 7df1276..6693080 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/key/Key.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/input/key/Key.desktop.kt
@@ -52,28 +52,28 @@
*
* May also be synthesized from trackball motions.
*/
- actual val DirectionUp = Key(KeyEvent.VK_KP_UP)
+ actual val DirectionUp = Key(KeyEvent.VK_UP)
/**
* Down Arrow Key / Directional Pad Down key.
*
* May also be synthesized from trackball motions.
*/
- actual val DirectionDown = Key(KeyEvent.VK_KP_DOWN)
+ actual val DirectionDown = Key(KeyEvent.VK_DOWN)
/**
* Left Arrow Key / Directional Pad Left key.
*
* May also be synthesized from trackball motions.
*/
- actual val DirectionLeft = Key(KeyEvent.VK_KP_LEFT)
+ actual val DirectionLeft = Key(KeyEvent.VK_LEFT)
/**
* Right Arrow Key / Directional Pad Right key.
*
* May also be synthesized from trackball motions.
*/
- actual val DirectionRight = Key(KeyEvent.VK_KP_RIGHT)
+ actual val DirectionRight = Key(KeyEvent.VK_RIGHT)
/** '0' key. */
actual val Zero = Key(KeyEvent.VK_0)
@@ -307,7 +307,7 @@
actual val PageUp = Key(KeyEvent.VK_PAGE_UP)
/** Page Down key. */
- actual val PageDown = Key(KeyEvent.VK_PAGE_UP)
+ actual val PageDown = Key(KeyEvent.VK_PAGE_DOWN)
/** F1 key. */
actual val F1 = Key(KeyEvent.VK_F1)
@@ -413,6 +413,9 @@
/** Numeric keypad ')' key. */
actual val NumPadRightParenthesis = Key(KeyEvent.VK_RIGHT_PARENTHESIS, KEY_LOCATION_NUMPAD)
+ actual val MoveHome = Key(KeyEvent.VK_HOME)
+ actual val MoveEnd = Key(KeyEvent.VK_END)
+
// Unsupported Keys. These keys will never be sent by the desktop. However we need unique
// keycodes so that these constants can be used in a when statement without a warning.
actual val SoftLeft = Key(-1000000001)
@@ -443,8 +446,6 @@
actual val Envelope = Key(-1000000026)
actual val Function = Key(-1000000027)
actual val Break = Key(-1000000028)
- actual val MoveHome = Key(-1000000029)
- actual val MoveEnd = Key(-1000000030)
actual val Number = Key(-1000000031)
actual val HeadsetHook = Key(-1000000032)
actual val Focus = Key(-1000000033)
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.desktop.kt
index 934230e..b496be2 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.desktop.kt
@@ -19,6 +19,7 @@
package androidx.compose.ui.platform
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
@@ -170,6 +171,13 @@
get() = container.keyboard
override fun sendKeyEvent(keyEvent: KeyEvent): Boolean {
+ when {
+ keyEvent.nativeKeyEvent.id == java.awt.event.KeyEvent.KEY_TYPED ->
+ container.platformInputService.charKeyPressed = true
+ keyEvent.type == KeyEventType.KeyDown ->
+ container.platformInputService.charKeyPressed = false
+ }
+
return keyInputModifier.processKeyInput(keyEvent) ||
keyboard?.processKeyInput(keyEvent) ?: false
}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.desktop.kt
index bbb543a..2ab89f4 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.desktop.kt
@@ -153,24 +153,15 @@
lastOwner?.onPointerMove(position)
}
- private fun consumeKeyEventOr(event: KeyEvent, or: () -> Unit) {
- val consumed = list.lastOrNull()?.sendKeyEvent(ComposeKeyEvent(event)) ?: false
- if (!consumed) {
- or()
- }
+ private fun consumeKeyEvent(event: KeyEvent) {
+ list.lastOrNull()?.sendKeyEvent(ComposeKeyEvent(event))
}
- fun onKeyPressed(event: KeyEvent) = consumeKeyEventOr(event) {
- platformInputService.onKeyPressed(event.keyCode, event.keyChar)
- }
+ fun onKeyPressed(event: KeyEvent) = consumeKeyEvent(event)
- fun onKeyReleased(event: KeyEvent) = consumeKeyEventOr(event) {
- platformInputService.onKeyReleased(event.keyCode, event.keyChar)
- }
+ fun onKeyReleased(event: KeyEvent) = consumeKeyEvent(event)
- fun onKeyTyped(event: KeyEvent) = consumeKeyEventOr(event) {
- platformInputService.onKeyTyped(event.keyChar)
- }
+ fun onKeyTyped(event: KeyEvent) = consumeKeyEvent(event)
fun onInputMethodEvent(event: InputMethodEvent) {
if (!event.isConsumed()) {
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt
index e057d3a..9cc2205 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt
@@ -16,13 +16,11 @@
package androidx.compose.ui.platform
import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.text.input.BackspaceCommand
import androidx.compose.ui.text.input.CommitTextCommand
import androidx.compose.ui.text.input.DeleteSurroundingTextInCodePointsCommand
import androidx.compose.ui.text.input.EditCommand
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.ImeOptions
-import androidx.compose.ui.text.input.MoveCursorCommand
import androidx.compose.ui.text.input.PlatformTextInputService
import androidx.compose.ui.text.input.SetComposingTextCommand
import androidx.compose.ui.text.input.TextFieldValue
@@ -31,7 +29,6 @@
import java.awt.Point
import java.awt.Rectangle
import java.awt.event.InputMethodEvent
-import java.awt.event.KeyEvent
import java.awt.font.TextHitInfo
import java.awt.im.InputMethodRequests
import java.text.AttributedCharacterIterator
@@ -61,6 +58,10 @@
var currentInput: CurrentInput? = null
+ // This is required to support input of accented characters using press-and-hold method (http://support.apple.com/kb/PH11264).
+ // JDK currently properly supports this functionality only for TextComponent/JTextComponent descendants.
+ // For our editor component we need this workaround.
+ // After https://bugs.openjdk.java.net/browse/JDK-8074882 is fixed, this workaround should be replaced with a proper solution.
var charKeyPressed: Boolean = false
var needToDeletePreviousChar: Boolean = false
@@ -103,57 +104,6 @@
}
}
- @Suppress("UNUSED_PARAMETER")
- fun onKeyPressed(keyCode: Int, char: Char) {
- if (keyCode >= KeyEvent.VK_A && keyCode <= KeyEvent.VK_Z) {
- charKeyPressed = true
- }
- currentInput?.let { input ->
- val command = input.onEditCommand
- when (keyCode) {
- KeyEvent.VK_LEFT -> {
- command.invoke(listOf(MoveCursorCommand(-1)))
- }
- KeyEvent.VK_RIGHT -> {
- command.invoke(listOf(MoveCursorCommand(1)))
- }
- KeyEvent.VK_BACK_SPACE -> {
- command.invoke(listOf(BackspaceCommand()))
- }
- KeyEvent.VK_ENTER -> {
- if (input.imeAction == ImeAction.Default) {
- command.invoke(listOf(CommitTextCommand("\n", 1)))
- } else {
- input.onImeActionPerformed.invoke(input.imeAction)
- }
- }
- else -> Unit
- }
- }
- }
-
- @Suppress("UNUSED_PARAMETER")
- fun onKeyReleased(keyCode: Int, char: Char) {
- charKeyPressed = false
- }
-
- private fun Char.isPrintable(): Boolean {
- val block = Character.UnicodeBlock.of(this)
- return (!Character.isISOControl(this)) &&
- this != KeyEvent.CHAR_UNDEFINED &&
- block != null &&
- block != Character.UnicodeBlock.SPECIALS
- }
-
- fun onKeyTyped(char: Char) {
- needToDeletePreviousChar = false
- currentInput?.onEditCommand?.let {
- if (char.isPrintable()) {
- it.invoke(listOf(CommitTextCommand(char.toString(), 1)))
- }
- }
- }
-
internal fun inputMethodCaretPositionChanged(
@Suppress("UNUSED_PARAMETER") event: InputMethodEvent
) {
@@ -172,10 +122,6 @@
val ops = mutableListOf<EditCommand>()
if (needToDeletePreviousChar && isMac) {
- // This is required to support input of accented characters using press-and-hold method (http://support.apple.com/kb/PH11264).
- // JDK currently properly supports this functionality only for TextComponent/JTextComponent descendants.
- // For our editor component we need this workaround.
- // After https://bugs.openjdk.java.net/browse/JDK-8074882 is fixed, this workaround should be replaced with a proper solution.
needToDeletePreviousChar = false
ops.add(DeleteSurroundingTextInCodePointsCommand(1, 0))
}
@@ -230,9 +176,7 @@
override fun getSelectedText(
attributes: Array<AttributedCharacterIterator.Attribute>?
): AttributedCharacterIterator {
- if (charKeyPressed) {
- needToDeletePreviousChar = true
- }
+ needToDeletePreviousChar = charKeyPressed
val str = input.value.text.substring(input.value.selection)
return AttributedString(str).iterator
}
diff --git a/core/core/build.gradle b/core/core/build.gradle
index 3f0fdc5..f776c9cf 100644
--- a/core/core/build.gradle
+++ b/core/core/build.gradle
@@ -10,7 +10,7 @@
}
dependencies {
- api(project(":annotation:annotation"))
+ api("androidx.annotation:annotation:1.2.0-rc01")
api("androidx.lifecycle:lifecycle-runtime:2.0.0")
api("androidx.versionedparcelable:versionedparcelable:1.1.1")
implementation("androidx.collection:collection:1.0.0")
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index a0fa20e..f0929b2 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -525,13 +525,15 @@
# > Task :compose:ui:ui:processDebugUnitTestManifest
\$OUT_DIR/androidx/compose/ui/ui/build/intermediates/tmp/manifest/test/debug/manifestMerger[0-9]+\.xml Warning:
# > Task *:lintDebug
-\$SUPPORT/.*/build\.gradle: Ignore: Unknown issue id "ComposableNaming" \[UnknownIssueId\]
-\$SUPPORT/.*/build\.gradle: Ignore: Unknown issue id "ComposableLambdaParameterNaming" \[UnknownIssueId\]
-\$SUPPORT/.*/build\.gradle: Ignore: Unknown issue id "ComposableLambdaParameterPosition" \[UnknownIssueId\]
-\$SUPPORT/.*/build\.gradle: Ignore: Unknown issue id "CompositionLocalNaming" \[UnknownIssueId\]
-Explanation for issues of type "UnknownIssueId":
-Lint will report this issue if it is configured with an issue id it does
-not recognize in for example Gradle files or lint\.xml configuration files\.
+# TODO: b/182321297 remove when this message isn't printed
+Lint: Unknown issue id "ComposableNaming"
+Lint: Unknown issue id "ComposableLambdaParameterNaming"
+Lint: Unknown issue id "ComposableLambdaParameterPosition"
+Lint: Unknown issue id "CompositionLocalNaming"
+Lint: Unknown issue id "ComposableModifierFactory"
+Lint: Unknown issue id "ModifierFactoryExtensionFunction"
+Lint: Unknown issue id "ModifierFactoryReturnType"
+Lint: Unknown issue id "ModifierParameter"
# > Task :compose:foundation:foundation:reportLibraryMetrics
Stripped invalid locals information from [0-9]+ methods\.
Methods with invalid locals information:
@@ -595,3 +597,11 @@
\$OUT_DIR\/androidx\/compose\/foundation\/foundation\-layout\/foundation\-layout\-benchmark\/build\/intermediates\/tmp\/manifest\/androidTest\/release\/manifestMerger[0-9]+\.xml\:[0-9]+\:[0-9]+\-[0-9]+\:[0-9]+ Warning\:
# > Task :compose:ui:ui-text:ui-text-benchmark:processReleaseAndroidTestManifest
\$OUT_DIR\/androidx\/compose\/ui\/ui\-text\/ui\-text\-benchmark\/build\/intermediates\/tmp\/manifest\/androidTest\/release\/manifestMerger[0-9]+\.xml\:[0-9]+\:[0-9]+\-[0-9]+\:[0-9]+ Warning\:
+# > Task :compose:benchmark-utils:benchmark-utils-benchmark:processReleaseAndroidTestManifest
+\$OUT_DIR\/androidx\/compose\/benchmark\-utils\/benchmark\-utils\-benchmark\/build\/intermediates\/tmp\/manifest\/androidTest\/release\/manifestMerger[0-9]+\.xml\:[0-9]+\:[0-9]+\-[0-9]+\:[0-9]+ Warning\:
+# > Task :compose:ui:ui-benchmark:processReleaseAndroidTestManifest
+\$OUT_DIR\/androidx\/compose\/ui\/ui\-benchmark\/build\/intermediates\/tmp\/manifest\/androidTest\/release\/manifestMerger[0-9]+\.xml\:[0-9]+\:[0-9]+\-[0-9]+\:[0-9]+ Warning\:
+# > Task :compose:animation:animation-core:animation-core-benchmark:processReleaseAndroidTestManifest
+\$OUT_DIR\/androidx\/compose\/animation\/animation\-core\/animation\-core\-benchmark\/build\/intermediates\/tmp\/manifest\/androidTest\/release\/manifestMerger[0-9]+\.xml\:[0-9]+\:[0-9]+\-[0-9]+\:[0-9]+ Warning\:
+# > Task :compose:ui:ui-graphics:ui-graphics-benchmark:processReleaseAndroidTestManifest
+\$OUT_DIR\/androidx\/compose\/ui\/ui\-graphics\/ui\-graphics\-benchmark\/build\/intermediates\/tmp\/manifest\/androidTest\/release\/manifestMerger[0-9]+\.xml\:[0-9]+\:[0-9]+\-[0-9]+\:[0-9]+ Warning\:
diff --git a/development/file-utils/diff-filterer.py b/development/file-utils/diff-filterer.py
index d1329bf..dd575a8 100755
--- a/development/file-utils/diff-filterer.py
+++ b/development/file-utils/diff-filterer.py
@@ -715,10 +715,17 @@
def cleanupTempDirs(self):
print("Clearing work directories")
- if os.path.isdir(self.workPath):
- for child in os.listdir(self.workPath):
- if child.startswith("job-"):
- fileIo.removePath(os.path.join(self.workPath, child))
+ numAttempts = 3
+ for attempt in range(numAttempts):
+ if os.path.isdir(self.workPath):
+ for child in os.listdir(self.workPath):
+ if child.startswith("job-"):
+ path = os.path.join(self.workPath, child)
+ try:
+ fileIo.removePath(path)
+ except IOError as e:
+ if attempt >= numAttempts - 1:
+ raise Exception("Failed to remove " + path, e)
def runnerTest(self, testState, timeout = None):
workPath = self.getWorkPath(0)
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index ea9d32c..20a697b 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -81,10 +81,10 @@
docs("androidx.contentpager:contentpager:1.0.0")
docs("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
docs("androidx.core:core-role:1.1.0-alpha02")
- docs("androidx.core:core:1.5.0-beta02")
+ docs("androidx.core:core:1.5.0-beta03")
docs("androidx.core:core-animation:1.0.0-alpha02")
docs("androidx.core:core-animation-testing:1.0.0-alpha02")
- docs("androidx.core:core-ktx:1.5.0-beta02")
+ docs("androidx.core:core-ktx:1.5.0-beta03")
docs("androidx.cursoradapter:cursoradapter:1.0.0")
docs("androidx.customview:customview:1.1.0")
docs("androidx.datastore:datastore:1.0.0-alpha08")
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 69bc9e6..9be8054 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -244,6 +244,9 @@
docs(project(":wear:wear-ongoing"))
docs(project(":wear:wear-phone-interactions"))
docs(project(":wear:wear-remote-interactions"))
+ docs(project(":wear:wear-tiles"))
+ docs(project(":wear:wear-tiles-proto"))
+ docs(project(":wear:wear-tiles-renderer"))
docs(project(":wear:wear-watchface"))
docs(project(":wear:wear-watchface-complications-rendering"))
docs(project(":wear:wear-watchface-client"))
diff --git a/emoji2/emoji2-bundled/build.gradle b/emoji2/emoji2-bundled/build.gradle
index 5b6e687..3c06a08 100644
--- a/emoji2/emoji2-bundled/build.gradle
+++ b/emoji2/emoji2-bundled/build.gradle
@@ -19,7 +19,7 @@
}
dependencies {
- api(project(":emoji"))
+ api(project(":emoji2:emoji2"))
}
androidx {
diff --git a/emoji2/emoji2-bundled/lint-baseline.xml b/emoji2/emoji2-bundled/lint-baseline.xml
deleted file mode 100644
index 27e26a8..0000000
--- a/emoji2/emoji2-bundled/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
-
-</issues>
diff --git a/emoji2/emoji2-bundled/src/main/java/androidx/emoji/bundled/BundledEmojiCompatConfig.java b/emoji2/emoji2-bundled/src/main/java/androidx/emoji2/bundled/BundledEmojiCompatConfig.java
similarity index 95%
rename from emoji2/emoji2-bundled/src/main/java/androidx/emoji/bundled/BundledEmojiCompatConfig.java
rename to emoji2/emoji2-bundled/src/main/java/androidx/emoji2/bundled/BundledEmojiCompatConfig.java
index 03f5567..5394c9b 100644
--- a/emoji2/emoji2-bundled/src/main/java/androidx/emoji/bundled/BundledEmojiCompatConfig.java
+++ b/emoji2/emoji2-bundled/src/main/java/androidx/emoji2/bundled/BundledEmojiCompatConfig.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.emoji.bundled;
+package androidx.emoji2.bundled;
import android.content.Context;
import android.content.res.AssetManager;
@@ -22,8 +22,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.util.Preconditions;
-import androidx.emoji.text.EmojiCompat;
-import androidx.emoji.text.MetadataRepo;
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.MetadataRepo;
/**
* {@link EmojiCompat.Config} implementation that loads the metadata using AssetManager and
diff --git a/emoji2/emoji2-views-core/AndroidManifest.xml b/emoji2/emoji2-views-core/AndroidManifest.xml
new file mode 100644
index 0000000..2f4b90a5
--- /dev/null
+++ b/emoji2/emoji2-views-core/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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.emoji2.helpers"/>
diff --git a/emoji2/emoji2-views-core/build.gradle b/emoji2/emoji2-views-core/build.gradle
new file mode 100644
index 0000000..6137849
--- /dev/null
+++ b/emoji2/emoji2-views-core/build.gradle
@@ -0,0 +1,46 @@
+import androidx.build.BundleInsideHelper
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.Publish
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("com.github.johnrengelman.shadow")
+}
+
+dependencies {
+ implementation(project(":emoji2:emoji2"))
+
+ api("androidx.core:core:1.3.0-rc01")
+ implementation("androidx.collection:collection:1.1.0")
+
+ androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
+ androidTestImplementation(ANDROIDX_TEST_CORE)
+ androidTestImplementation(ANDROIDX_TEST_RUNNER)
+ androidTestImplementation(ANDROIDX_TEST_RULES)
+ androidTestImplementation(ESPRESSO_CORE, libs.exclude_for_espresso)
+ androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation project(':internal-testutils-runtime')
+}
+
+android {
+ sourceSets {
+ main {
+ // We use a non-standard manifest path.
+ manifest.srcFile 'AndroidManifest.xml'
+ }
+ }
+}
+
+androidx {
+ name = "Android Emoji2 Compat view helpers"
+ publish = Publish.NONE
+ mavenVersion = LibraryVersions.EMOJI2
+ mavenGroup = LibraryGroups.EMOJI2
+ inceptionYear = "2017"
+ description = "View helpers for Emoji2"
+}
diff --git a/emoji2/emoji2-views-core/src/androidTest/AndroidManifest.xml b/emoji2/emoji2-views-core/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..6c30771
--- /dev/null
+++ b/emoji2/emoji2-views-core/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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.emoji2.helpers">
+
+ <application>
+ </application>
+
+</manifest>
\ No newline at end of file
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextHelperPre19Test.java b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiEditTextHelperPre19Test.java
similarity index 98%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextHelperPre19Test.java
rename to emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiEditTextHelperPre19Test.java
index 2883836..c431518 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextHelperPre19Test.java
+++ b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiEditTextHelperPre19Test.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.emoji2.widget;
+package androidx.emoji2.helpers;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextHelperTest.java b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiEditTextHelperTest.java
similarity index 99%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextHelperTest.java
rename to emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiEditTextHelperTest.java
index f995697..c054c74 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextHelperTest.java
+++ b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiEditTextHelperTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.emoji2.widget;
+package androidx.emoji2.helpers;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.junit.Assert.assertEquals;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditableFactoryTest.java b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiEditableFactoryTest.java
similarity index 97%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditableFactoryTest.java
rename to emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiEditableFactoryTest.java
index 7addec0..36200b0 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditableFactoryTest.java
+++ b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiEditableFactoryTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.widget;
+package androidx.emoji2.helpers;
import static org.hamcrest.Matchers.arrayWithSize;
import static org.hamcrest.Matchers.instanceOf;
@@ -30,6 +30,7 @@
import androidx.emoji2.text.EmojiMetadata;
import androidx.emoji2.text.EmojiSpan;
+import androidx.emoji2.text.SpannableBuilder;
import androidx.emoji2.text.TypefaceEmojiSpan;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
diff --git a/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiInputConnectionTest.java b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiInputConnectionTest.java
new file mode 100644
index 0000000..b963cba
--- /dev/null
+++ b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiInputConnectionTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.emoji2.helpers;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.Build;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.SpannableStringBuilder;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.TextView;
+
+import androidx.emoji2.util.Emoji;
+import androidx.emoji2.util.TestString;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = 19)
+public class EmojiInputConnectionTest {
+
+ private InputConnection mInputConnection;
+ private TestString mTestString;
+ private Editable mEditable;
+ private EmojiInputConnection mEmojiEmojiInputConnection;
+ private EmojiInputConnection.EmojiCompatDeleteHelper mEmojiCompatDeleteHelper;
+
+ @Before
+ public void setup() {
+ mTestString = new TestString(Emoji.EMOJI_WITH_ZWJ).withPrefix().withSuffix();
+ mEditable = new SpannableStringBuilder(mTestString.toString());
+ mInputConnection = mock(InputConnection.class);
+ final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ final TextView textView = spy(new TextView(context));
+ mEmojiCompatDeleteHelper = mock(EmojiInputConnection.EmojiCompatDeleteHelper.class);
+ doNothing().when(mEmojiCompatDeleteHelper).updateEditorInfoAttrs(any());
+
+ doReturn(mEditable).when(textView).getEditableText();
+ when(mInputConnection.deleteSurroundingText(anyInt(), anyInt())).thenReturn(false);
+ setupDeleteSurroundingText();
+
+ mEmojiEmojiInputConnection = new EmojiInputConnection(textView, mInputConnection,
+ new EditorInfo(), mEmojiCompatDeleteHelper);
+ }
+
+ private void setupDeleteSurroundingText() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ when(mInputConnection.deleteSurroundingTextInCodePoints(anyInt(), anyInt())).thenReturn(
+ false);
+ }
+ }
+
+ @Test
+ public void whenEmojiCompatDelete_doesntDelete_inputConnectionIsCalled() {
+ Selection.setSelection(mEditable, 0, mEditable.length());
+ when(mEmojiCompatDeleteHelper.handleDeleteSurroundingText(any(), any(), anyInt(),
+ anyInt(), anyBoolean())).thenReturn(false);
+ assertFalse(mEmojiEmojiInputConnection.deleteSurroundingText(1, 0));
+ verify(mInputConnection, times(1)).deleteSurroundingText(1, 0);
+ }
+
+ @Test
+ public void whenEmojiCompatDelete_doesDelete_inputConnectionIsNotCalled() {
+ Selection.setSelection(mEditable, mTestString.emojiEndIndex());
+ when(mEmojiCompatDeleteHelper.handleDeleteSurroundingText(any(), any(), anyInt(),
+ anyInt(), anyBoolean())).thenReturn(true);
+ assertTrue(mEmojiEmojiInputConnection.deleteSurroundingText(1, 0));
+ verify(mInputConnection, never()).deleteSurroundingText(anyInt(), anyInt());
+ }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiInputFilterTest.java b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiInputFilterTest.java
similarity index 98%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiInputFilterTest.java
rename to emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiInputFilterTest.java
index dc9b977..1fe748d 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiInputFilterTest.java
+++ b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiInputFilterTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.widget;
+package androidx.emoji2.helpers;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
diff --git a/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiKeyListenerTest.java b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiKeyListenerTest.java
new file mode 100644
index 0000000..e623691
--- /dev/null
+++ b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiKeyListenerTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.emoji2.helpers;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.mock;
+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.text.Editable;
+import android.text.Selection;
+import android.text.SpannableStringBuilder;
+import android.text.method.KeyListener;
+import android.view.KeyEvent;
+import android.view.View;
+
+import androidx.emoji2.util.Emoji;
+import androidx.emoji2.util.KeyboardUtil;
+import androidx.emoji2.util.TestString;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = 19)
+public class EmojiKeyListenerTest {
+
+ private KeyListener mKeyListener;
+ private TestString mTestString;
+ private Editable mEditable;
+ private EmojiKeyListener mEmojiKeyListener;
+ private EmojiKeyListener.EmojiCompatHandleKeyDownHelper mEmojiCompatKeydownHelper;
+
+ @Before
+ public void setup() {
+ mKeyListener = mock(KeyListener.class);
+ mTestString = new TestString(Emoji.EMOJI_WITH_ZWJ).withPrefix().withSuffix();
+ mEditable = new SpannableStringBuilder(mTestString.toString());
+ mEmojiCompatKeydownHelper = mock(EmojiKeyListener.EmojiCompatHandleKeyDownHelper.class);
+ mEmojiKeyListener = new EmojiKeyListener(mKeyListener, mEmojiCompatKeydownHelper);
+
+ when(mKeyListener.onKeyDown(any(View.class), any(Editable.class), anyInt(),
+ any(KeyEvent.class))).thenReturn(false);
+ }
+
+ @Test
+ public void whenEmojiCompat_handlesKeyDown_doesntCallKeyListener() {
+ Selection.setSelection(mEditable, 0);
+ final KeyEvent event = KeyboardUtil.zero();
+ when(mEmojiCompatKeydownHelper.handleKeyDown(any(), anyInt(), any())).thenReturn(true);
+ assertTrue(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
+ verifyNoMoreInteractions(mKeyListener);
+ }
+
+ @Test
+ public void whenEmojiCompatDoesnt_handleKeyDown_callsListener() {
+ Selection.setSelection(mEditable, 0);
+ final KeyEvent event = KeyboardUtil.zero();
+ when(mEmojiCompatKeydownHelper.handleKeyDown(any(), anyInt(), any())).thenReturn(false);
+ assertFalse(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
+ verify(mKeyListener, times(1)).onKeyDown((View) eq(null), same(mEditable),
+ eq(event.getKeyCode()), same(event));
+ }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTextViewHelperPre19Test.java b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiTextViewHelperPre19Test.java
similarity index 98%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTextViewHelperPre19Test.java
rename to emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiTextViewHelperPre19Test.java
index eaa6a01..6a9932a 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTextViewHelperPre19Test.java
+++ b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiTextViewHelperPre19Test.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.emoji2.widget;
+package androidx.emoji2.helpers;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTextViewHelperTest.java b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiTextViewHelperTest.java
similarity index 97%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTextViewHelperTest.java
rename to emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiTextViewHelperTest.java
index d4b00e9..51b71cf 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTextViewHelperTest.java
+++ b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiTextViewHelperTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.emoji2.widget;
+package androidx.emoji2.helpers;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.instanceOf;
@@ -35,6 +35,7 @@
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
+import org.hamcrest.CoreMatchers;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -114,7 +115,7 @@
assertEquals(2, newFilters.length);
assertThat(Arrays.asList(newFilters), hasItem(existingFilter));
- assertThat(Arrays.asList(newFilters), hasItem(emojiInputFilter));
+ assertThat(Arrays.asList(newFilters), CoreMatchers.hasItem(emojiInputFilter));
}
private EmojiInputFilter findEmojiInputFilter(final InputFilter[] filters) {
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTextWatcherTest.java b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiTextWatcherTest.java
similarity index 98%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTextWatcherTest.java
rename to emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiTextWatcherTest.java
index 35b5d2e..e1cf65e 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTextWatcherTest.java
+++ b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiTextWatcherTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.emoji2.widget;
+package androidx.emoji2.helpers;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTransformationMethodTest.java b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiTransformationMethodTest.java
similarity index 99%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTransformationMethodTest.java
rename to emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiTransformationMethodTest.java
index 31122c8..4b9d10f 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTransformationMethodTest.java
+++ b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/helpers/EmojiTransformationMethodTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.widget;
+package androidx.emoji2.helpers;
import static junit.framework.TestCase.assertSame;
diff --git a/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/util/Emoji.java b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/util/Emoji.java
new file mode 100644
index 0000000..73117a2
--- /dev/null
+++ b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/util/Emoji.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.util;
+
+import androidx.annotation.NonNull;
+
+public class Emoji {
+
+ public static final int CHAR_KEYCAP = 0x20E3;
+ public static final int CHAR_DIGIT = 0x0039;
+ public static final int CHAR_ZWJ = 0x200D;
+ public static final int CHAR_VS_EMOJI = 0xFE0f;
+ public static final int CHAR_VS_TEXT = 0xFE0E;
+ public static final int CHAR_FITZPATRICK = 0x1F3FE;
+ public static final int CHAR_FITZPATRICK_TYPE_1 = 0x1F3fB;
+ public static final int CHAR_DEFAULT_TEXT_STYLE = 0x26F9;
+ public static final int CHAR_DEFAULT_EMOJI_STYLE = 0x1f3A2;
+ public static final int CHAR_FEMALE_SIGN = 0x2640;
+ public static final int CHAR_MAN = 0x1F468;
+ public static final int CHAR_HEART = 0x2764;
+ public static final int CHAR_KISS = 0x1F48B;
+ public static final int CHAR_REGIONAL_SYMBOL = 0x1F1E8;
+ public static final int CHAR_ASTERISK = 0x002A;
+
+ public static final EmojiMapping EMOJI_SINGLE_CODEPOINT = new EmojiMapping(
+ new int[]{CHAR_DEFAULT_EMOJI_STYLE}, 0xF01B4);
+
+ public static final EmojiMapping EMOJI_WITH_ZWJ = new EmojiMapping(
+ new int[]{CHAR_MAN, CHAR_ZWJ, CHAR_HEART, CHAR_VS_EMOJI, CHAR_ZWJ, CHAR_KISS, CHAR_ZWJ,
+ CHAR_MAN}, 0xF051F);
+
+ public static final EmojiMapping EMOJI_GENDER = new EmojiMapping(new int[]{
+ CHAR_DEFAULT_TEXT_STYLE, CHAR_VS_EMOJI, CHAR_ZWJ, CHAR_FEMALE_SIGN}, 0xF0950);
+
+ public static final EmojiMapping EMOJI_FLAG = new EmojiMapping(
+ new int[]{CHAR_REGIONAL_SYMBOL, CHAR_REGIONAL_SYMBOL}, 0xF03A0);
+
+ public static final EmojiMapping EMOJI_GENDER_WITHOUT_VS = new EmojiMapping(
+ new int[]{CHAR_DEFAULT_TEXT_STYLE, CHAR_ZWJ, CHAR_FEMALE_SIGN}, 0xF0950);
+
+ public static final EmojiMapping DEFAULT_TEXT_STYLE = new EmojiMapping(
+ new int[]{CHAR_DEFAULT_TEXT_STYLE, CHAR_VS_EMOJI}, 0xF04C6);
+
+ public static final EmojiMapping EMOJI_REGIONAL_SYMBOL = new EmojiMapping(
+ new int[]{CHAR_REGIONAL_SYMBOL}, 0xF0025);
+
+ public static final EmojiMapping EMOJI_UNKNOWN_FLAG = new EmojiMapping(
+ new int[]{0x1F1FA, 0x1F1F3}, 0xF0599);
+
+ public static final EmojiMapping EMOJI_DIGIT_ES = new EmojiMapping(
+ new int[]{CHAR_DIGIT, CHAR_VS_EMOJI}, 0xF0340);
+
+ public static final EmojiMapping EMOJI_DIGIT_KEYCAP = new EmojiMapping(
+ new int[]{CHAR_DIGIT, CHAR_KEYCAP}, 0xF0377);
+
+ public static final EmojiMapping EMOJI_DIGIT_ES_KEYCAP = new EmojiMapping(
+ new int[]{CHAR_DIGIT, CHAR_VS_EMOJI, CHAR_KEYCAP}, 0xF0377);
+
+ public static final EmojiMapping EMOJI_ASTERISK_KEYCAP = new EmojiMapping(
+ new int[]{CHAR_ASTERISK, CHAR_VS_EMOJI, CHAR_KEYCAP}, 0xF051D);
+
+ public static final EmojiMapping EMOJI_SKIN_MODIFIER = new EmojiMapping(
+ new int[]{CHAR_MAN, CHAR_FITZPATRICK}, 0xF0603);
+
+ public static final EmojiMapping EMOJI_SKIN_MODIFIER_TYPE_ONE = new EmojiMapping(
+ new int[]{CHAR_MAN, CHAR_FITZPATRICK_TYPE_1}, 0xF0606);
+
+ public static final EmojiMapping EMOJI_SKIN_MODIFIER_WITH_VS = new EmojiMapping(
+ new int[]{CHAR_MAN, CHAR_VS_EMOJI, CHAR_FITZPATRICK_TYPE_1}, 0xF0606);
+
+ public static class EmojiMapping {
+ private final int[] mCodepoints;
+ private final int mId;
+
+ private EmojiMapping(@NonNull final int[] codepoints, final int id) {
+ mCodepoints = codepoints;
+ mId = id;
+ }
+
+ public final int[] codepoints() {
+ return mCodepoints;
+ }
+
+ public final int id() {
+ return mId;
+ }
+
+ public final int charCount() {
+ int count = 0;
+ for (int i = 0; i < mCodepoints.length; i++) {
+ count += Character.charCount(mCodepoints[i]);
+ }
+ return count;
+ }
+ }
+}
diff --git a/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/util/EmojiMatcher.java b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/util/EmojiMatcher.java
new file mode 100644
index 0000000..32d8e03
--- /dev/null
+++ b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/util/EmojiMatcher.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.emoji2.util;
+
+import static org.mockito.ArgumentMatchers.argThat;
+
+import android.text.Spanned;
+import android.text.TextUtils;
+
+import androidx.emoji2.text.EmojiSpan;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+import org.mockito.ArgumentMatcher;
+
+/**
+ * Utility class that includes matchers specific to emojis and EmojiSpans.
+ */
+public class EmojiMatcher {
+
+ public static Matcher<CharSequence> hasEmojiAt(final int id, final int start,
+ final int end) {
+ return new EmojiResourceMatcher(id, start, end);
+ }
+
+ public static Matcher<CharSequence> hasEmojiAt(final Emoji.EmojiMapping emojiMapping,
+ final int start, final int end) {
+ return new EmojiResourceMatcher(emojiMapping.id(), start, end);
+ }
+
+ public static Matcher<CharSequence> hasEmojiAt(final int start, final int end) {
+ return new EmojiResourceMatcher(-1, start, end);
+ }
+
+ public static Matcher<CharSequence> hasEmoji(final int id) {
+ return new EmojiResourceMatcher(id, -1, -1);
+ }
+
+ public static Matcher<CharSequence> hasEmoji(final Emoji.EmojiMapping emojiMapping) {
+ return new EmojiResourceMatcher(emojiMapping.id(), -1, -1);
+ }
+
+ public static Matcher<CharSequence> hasEmoji() {
+ return new EmojiSpanMatcher();
+ }
+
+ public static Matcher<CharSequence> hasEmojiCount(final int count) {
+ return new EmojiCountMatcher(count);
+ }
+
+ public static <T extends CharSequence> T sameCharSequence(final T expected) {
+ return argThat(new ArgumentMatcher<T>() {
+ @Override
+ public boolean matches(T o) {
+ if (o instanceof CharSequence) {
+ return TextUtils.equals(expected, o);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "doesn't match " + expected;
+ }
+ });
+ }
+
+ private static class EmojiSpanMatcher extends TypeSafeMatcher<CharSequence> {
+
+ private EmojiSpan[] mSpans;
+
+ EmojiSpanMatcher() {
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("should have EmojiSpans");
+ }
+
+ @Override
+ protected void describeMismatchSafely(final CharSequence charSequence,
+ Description mismatchDescription) {
+ mismatchDescription.appendText(" has no EmojiSpans");
+ }
+
+ @Override
+ protected boolean matchesSafely(final CharSequence charSequence) {
+ if (charSequence == null) return false;
+ if (!(charSequence instanceof Spanned)) return false;
+ mSpans = ((Spanned) charSequence).getSpans(0, charSequence.length(), EmojiSpan.class);
+ return mSpans.length != 0;
+ }
+ }
+
+ private static class EmojiCountMatcher extends TypeSafeMatcher<CharSequence> {
+
+ private final int mCount;
+ private EmojiSpan[] mSpans;
+
+ EmojiCountMatcher(final int count) {
+ mCount = count;
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("should have ").appendValue(mCount).appendText(" EmojiSpans");
+ }
+
+ @Override
+ protected void describeMismatchSafely(final CharSequence charSequence,
+ Description mismatchDescription) {
+ mismatchDescription.appendText(" has ");
+ if (mSpans == null) {
+ mismatchDescription.appendValue("no");
+ } else {
+ mismatchDescription.appendValue(mSpans.length);
+ }
+
+ mismatchDescription.appendText(" EmojiSpans");
+ }
+
+ @Override
+ protected boolean matchesSafely(final CharSequence charSequence) {
+ if (charSequence == null) return false;
+ if (!(charSequence instanceof Spanned)) return false;
+ mSpans = ((Spanned) charSequence).getSpans(0, charSequence.length(), EmojiSpan.class);
+ return mSpans.length == mCount;
+ }
+ }
+
+ private static class EmojiResourceMatcher extends TypeSafeMatcher<CharSequence> {
+ private static final int ERR_NONE = 0;
+ private static final int ERR_SPANNABLE_NULL = 1;
+ private static final int ERR_NO_SPANS = 2;
+ private static final int ERR_WRONG_INDEX = 3;
+ private final int mResId;
+ private final int mStart;
+ private final int mEnd;
+ private int mError = ERR_NONE;
+ private int mActualStart = -1;
+ private int mActualEnd = -1;
+
+ EmojiResourceMatcher(int resId, int start, int end) {
+ mResId = resId;
+ mStart = start;
+ mEnd = end;
+ }
+
+ @Override
+ public void describeTo(final Description description) {
+ if (mResId == -1) {
+ description.appendText("should have EmojiSpan at ")
+ .appendValue("[" + mStart + "," + mEnd + "]");
+ } else if (mStart == -1 && mEnd == -1) {
+ description.appendText("should have EmojiSpan with resource id ")
+ .appendValue(Integer.toHexString(mResId));
+ } else {
+ description.appendText("should have EmojiSpan with resource id ")
+ .appendValue(Integer.toHexString(mResId))
+ .appendText(" at ")
+ .appendValue("[" + mStart + "," + mEnd + "]");
+ }
+ }
+
+ @Override
+ protected void describeMismatchSafely(final CharSequence charSequence,
+ Description mismatchDescription) {
+ int offset = 0;
+ mismatchDescription.appendText("[");
+ while (offset < charSequence.length()) {
+ int codepoint = Character.codePointAt(charSequence, offset);
+ mismatchDescription.appendText(Integer.toHexString(codepoint));
+ offset += Character.charCount(codepoint);
+ if (offset < charSequence.length()) {
+ mismatchDescription.appendText(",");
+ }
+ }
+ mismatchDescription.appendText("]");
+
+ switch (mError) {
+ case ERR_NO_SPANS:
+ mismatchDescription.appendText(" had no spans");
+ break;
+ case ERR_SPANNABLE_NULL:
+ mismatchDescription.appendText(" was null");
+ break;
+ case ERR_WRONG_INDEX:
+ mismatchDescription.appendText(" had Emoji at ")
+ .appendValue("[" + mActualStart + "," + mActualEnd + "]");
+ break;
+ default:
+ mismatchDescription.appendText(" does not have an EmojiSpan with given "
+ + "resource id ");
+ }
+ }
+
+ @Override
+ protected boolean matchesSafely(final CharSequence charSequence) {
+ if (charSequence == null) {
+ mError = ERR_SPANNABLE_NULL;
+ return false;
+ }
+
+ if (!(charSequence instanceof Spanned)) {
+ mError = ERR_NO_SPANS;
+ return false;
+ }
+
+ Spanned spanned = (Spanned) charSequence;
+ final EmojiSpan[] spans = spanned.getSpans(0, charSequence.length(), EmojiSpan.class);
+
+ if (spans.length == 0) {
+ mError = ERR_NO_SPANS;
+ return false;
+ }
+
+ if (mStart == -1 && mEnd == -1) {
+ for (int index = 0; index < spans.length; index++) {
+ if (mResId == spans[index].getId()) {
+ return true;
+ }
+ }
+ return false;
+ } else {
+ for (int index = 0; index < spans.length; index++) {
+ if (mResId == -1 || mResId == spans[index].getId()) {
+ mActualStart = spanned.getSpanStart(spans[index]);
+ mActualEnd = spanned.getSpanEnd(spans[index]);
+ if (mActualStart == mStart && mActualEnd == mEnd) {
+ return true;
+ }
+ }
+ }
+
+ if (mActualStart != -1 && mActualEnd != -1) {
+ mError = ERR_WRONG_INDEX;
+ }
+
+ return false;
+ }
+ }
+ }
+}
diff --git a/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/util/KeyboardUtil.java b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/util/KeyboardUtil.java
new file mode 100644
index 0000000..48a6d2e
--- /dev/null
+++ b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/util/KeyboardUtil.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.emoji2.util;
+
+import android.app.Instrumentation;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.method.QwertyKeyListener;
+import android.text.method.TextKeyListener;
+import android.view.KeyEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.TextView;
+
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * Utility class for KeyEvents
+ */
+public class KeyboardUtil {
+ private static final int ALT = KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
+ private static final int CTRL = KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
+ private static final int SHIFT = KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON;
+ private static final int FN = KeyEvent.META_FUNCTION_ON;
+
+ public static KeyEvent zero() {
+ return keyEvent(KeyEvent.KEYCODE_0);
+ }
+
+ public static KeyEvent del() {
+ return keyEvent(KeyEvent.KEYCODE_DEL);
+ }
+
+ public static KeyEvent altDel() {
+ return keyEvent(KeyEvent.KEYCODE_DEL, ALT);
+ }
+
+ public static KeyEvent ctrlDel() {
+ return keyEvent(KeyEvent.KEYCODE_DEL, CTRL);
+ }
+
+ public static KeyEvent shiftDel() {
+ return keyEvent(KeyEvent.KEYCODE_DEL, SHIFT);
+ }
+
+ public static KeyEvent fnDel() {
+ return keyEvent(KeyEvent.KEYCODE_DEL, FN);
+ }
+
+ public static KeyEvent forwardDel() {
+ return keyEvent(KeyEvent.KEYCODE_FORWARD_DEL);
+ }
+
+ public static KeyEvent keyEvent(int keycode, int metaState) {
+ final long currentTime = System.currentTimeMillis();
+ return new KeyEvent(currentTime, currentTime, KeyEvent.ACTION_DOWN, keycode, 0, metaState);
+ }
+
+ public static KeyEvent keyEvent(int keycode) {
+ final long currentTime = System.currentTimeMillis();
+ return new KeyEvent(currentTime, currentTime, KeyEvent.ACTION_DOWN, keycode, 0);
+ }
+
+ public static void setComposingTextInBatch(final Instrumentation instrumentation,
+ final InputConnection inputConnection, final CharSequence text)
+ throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ instrumentation.runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ inputConnection.beginBatchEdit();
+ inputConnection.setComposingText(text, 1);
+ inputConnection.endBatchEdit();
+ latch.countDown();
+ }
+ });
+
+ latch.await();
+ instrumentation.waitForIdleSync();
+ }
+
+ public static void deleteSurroundingText(final Instrumentation instrumentation,
+ final InputConnection inputConnection, final int before, final int after)
+ throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ instrumentation.runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ inputConnection.beginBatchEdit();
+ inputConnection.deleteSurroundingText(before, after);
+ inputConnection.endBatchEdit();
+ latch.countDown();
+ }
+ });
+ latch.await();
+ instrumentation.waitForIdleSync();
+ }
+
+ public static void setSelection(Instrumentation instrumentation, final Spannable spannable,
+ final int start) throws InterruptedException {
+ setSelection(instrumentation, spannable, start, start);
+ }
+
+ public static void setSelection(Instrumentation instrumentation, final Spannable spannable,
+ final int start, final int end) throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ instrumentation.runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ Selection.setSelection(spannable, start, end);
+ latch.countDown();
+ }
+ });
+ latch.await();
+ instrumentation.waitForIdleSync();
+ }
+
+ public static InputConnection initTextViewForSimulatedIme(Instrumentation instrumentation,
+ final TextView textView) throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ instrumentation.runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ textView.setKeyListener(
+ QwertyKeyListener.getInstance(false, TextKeyListener.Capitalize.NONE));
+ textView.setText("", TextView.BufferType.EDITABLE);
+ latch.countDown();
+ }
+ });
+ latch.await();
+ instrumentation.waitForIdleSync();
+ return textView.onCreateInputConnection(new EditorInfo());
+ }
+}
diff --git a/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/util/TestString.java b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/util/TestString.java
new file mode 100644
index 0000000..83728da
--- /dev/null
+++ b/emoji2/emoji2-views-core/src/androidTest/java/androidx/emoji2/util/TestString.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.emoji2.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class used to create strings with emojis during tests.
+ */
+public class TestString {
+
+ private static final List<Integer> EMPTY_LIST = new ArrayList<>();
+
+ private static final String EXTRA = "ab";
+ private final List<Integer> mCodePoints;
+ private String mString;
+ private final String mValue;
+ private boolean mHasSuffix;
+ private boolean mHasPrefix;
+
+ public TestString(int... codePoints) {
+ if (codePoints.length == 0) {
+ mCodePoints = EMPTY_LIST;
+ } else {
+ mCodePoints = new ArrayList<>();
+ append(codePoints);
+ }
+ mValue = null;
+ }
+
+ public TestString(Emoji.EmojiMapping emojiMapping) {
+ this(emojiMapping.codepoints());
+ }
+
+ public TestString(String string) {
+ mCodePoints = EMPTY_LIST;
+ mValue = string;
+ }
+
+ public TestString append(int... codePoints) {
+ for (int i = 0; i < codePoints.length; i++) {
+ mCodePoints.add(codePoints[i]);
+ }
+ return this;
+ }
+
+ public TestString prepend(int... codePoints) {
+ for (int i = codePoints.length - 1; i >= 0; i--) {
+ mCodePoints.add(0, codePoints[i]);
+ }
+ return this;
+ }
+
+ public TestString append(Emoji.EmojiMapping emojiMapping) {
+ return append(emojiMapping.codepoints());
+ }
+
+ public TestString withSuffix() {
+ mHasSuffix = true;
+ return this;
+ }
+
+ public TestString withPrefix() {
+ mHasPrefix = true;
+ return this;
+ }
+
+ @SuppressWarnings("ForLoopReplaceableByForEach")
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ if (mHasPrefix) {
+ builder.append(EXTRA);
+ }
+
+ for (int index = 0; index < mCodePoints.size(); index++) {
+ builder.append(Character.toChars(mCodePoints.get(index)));
+ }
+
+ if (mValue != null) {
+ builder.append(mValue);
+ }
+
+ if (mHasSuffix) {
+ builder.append(EXTRA);
+ }
+ mString = builder.toString();
+ return mString;
+ }
+
+ public int emojiStartIndex() {
+ if (mHasPrefix) return EXTRA.length();
+ return 0;
+ }
+
+ public int emojiEndIndex() {
+ if (mHasSuffix) return mString.lastIndexOf(EXTRA);
+ return mString.length();
+ }
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiEditTextHelper.java b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiEditTextHelper.java
similarity index 96%
rename from emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiEditTextHelper.java
rename to emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiEditTextHelper.java
index 32ee18b..75ab24e 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiEditTextHelper.java
+++ b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiEditTextHelper.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.widget;
+package androidx.emoji2.helpers;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
@@ -31,6 +31,7 @@
import androidx.annotation.RestrictTo;
import androidx.core.util.Preconditions;
import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.EmojiDefaults;
import androidx.emoji2.text.EmojiSpan;
/**
@@ -70,7 +71,7 @@
*/
public final class EmojiEditTextHelper {
private final HelperInternal mHelper;
- private int mMaxEmojiCount = EditTextAttributeHelper.MAX_EMOJI_COUNT;
+ private int mMaxEmojiCount = EmojiDefaults.MAX_EMOJI_COUNT;
@EmojiCompat.ReplaceStrategy
private int mEmojiReplaceStrategy = EmojiCompat.REPLACE_STRATEGY_DEFAULT;
@@ -159,7 +160,7 @@
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
- void setEmojiReplaceStrategy(@EmojiCompat.ReplaceStrategy int replaceStrategy) {
+ public void setEmojiReplaceStrategy(@EmojiCompat.ReplaceStrategy int replaceStrategy) {
mEmojiReplaceStrategy = replaceStrategy;
mHelper.setEmojiReplaceStrategy(replaceStrategy);
}
@@ -174,7 +175,7 @@
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
- int getEmojiReplaceStrategy() {
+ public int getEmojiReplaceStrategy() {
return mEmojiReplaceStrategy;
}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiEditableFactory.java b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiEditableFactory.java
similarity index 96%
rename from emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiEditableFactory.java
rename to emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiEditableFactory.java
index b9c1abd..bb6a713f 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiEditableFactory.java
+++ b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiEditableFactory.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.widget;
+package androidx.emoji2.helpers;
import android.annotation.SuppressLint;
import android.text.Editable;
@@ -21,6 +21,7 @@
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.emoji2.text.SpannableBuilder;
/**
* EditableFactory used to improve editing operations on an EditText.
diff --git a/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiInputConnection.java b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiInputConnection.java
new file mode 100644
index 0000000..91dcb46
--- /dev/null
+++ b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiInputConnection.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.emoji2.helpers;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+
+import android.text.Editable;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputConnectionWrapper;
+import android.widget.TextView;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.emoji2.text.EmojiCompat;
+
+/**
+ * InputConnectionWrapper for EditText delete operations. Keyboard does not have knowledge about
+ * emojis and therefore might send commands to delete a part of the emoji sequence which creates
+ * invalid codeunits/getCodepointAt in the text.
+ * <p/>
+ * This class tries to correctly delete an emoji checking if there is an emoji span.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP_PREFIX)
+@RequiresApi(19)
+final class EmojiInputConnection extends InputConnectionWrapper {
+ private final TextView mTextView;
+ private final EmojiCompatDeleteHelper mEmojiCompatDeleteHelper;
+
+ EmojiInputConnection(
+ @NonNull final TextView textView,
+ @NonNull final InputConnection inputConnection,
+ @NonNull final EditorInfo outAttrs) {
+ this(textView, inputConnection, outAttrs, new EmojiCompatDeleteHelper());
+ }
+
+ EmojiInputConnection(
+ @NonNull final TextView textView,
+ @NonNull final InputConnection inputConnection,
+ @NonNull final EditorInfo outAttrs,
+ @NonNull final EmojiCompatDeleteHelper emojiCompatDeleteHelper
+ ) {
+ super(inputConnection, false);
+ mTextView = textView;
+ mEmojiCompatDeleteHelper = emojiCompatDeleteHelper;
+ mEmojiCompatDeleteHelper.updateEditorInfoAttrs(outAttrs);
+ }
+
+ @Override
+ public boolean deleteSurroundingText(final int beforeLength, final int afterLength) {
+ final boolean result = mEmojiCompatDeleteHelper.handleDeleteSurroundingText(
+ this, getEditable(), beforeLength, afterLength, false /*inCodePoints*/);
+ return result || super.deleteSurroundingText(beforeLength, afterLength);
+ }
+
+ @Override
+ public boolean deleteSurroundingTextInCodePoints(final int beforeLength,
+ final int afterLength) {
+ final boolean result = mEmojiCompatDeleteHelper.handleDeleteSurroundingText(
+ this, getEditable(), beforeLength, afterLength, true /*inCodePoints*/);
+ return result || super.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
+ }
+
+ private Editable getEditable() {
+ return mTextView.getEditableText();
+ }
+
+ public static class EmojiCompatDeleteHelper {
+ public boolean handleDeleteSurroundingText(
+ @NonNull final InputConnection inputConnection,
+ @NonNull final Editable editable,
+ @IntRange(from = 0) final int beforeLength,
+ @IntRange(from = 0) final int afterLength,
+ final boolean inCodePoints) {
+ return EmojiCompat.handleDeleteSurroundingText(inputConnection, editable,
+ beforeLength, afterLength, inCodePoints);
+ }
+
+ public void updateEditorInfoAttrs(@NonNull final EditorInfo outAttrs) {
+ EmojiCompat.get().updateEditorInfoAttrs(outAttrs);
+ }
+ }
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiInputFilter.java b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiInputFilter.java
similarity index 98%
rename from emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiInputFilter.java
rename to emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiInputFilter.java
index fb1bd11..ccb3033 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiInputFilter.java
+++ b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiInputFilter.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.widget;
+package androidx.emoji2.helpers;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
@@ -96,6 +96,7 @@
return mInitCallback;
}
+ @RequiresApi(19)
private static class InitCallbackImpl extends InitCallback {
private final Reference<TextView> mViewRef;
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiKeyListener.java b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiKeyListener.java
similarity index 70%
rename from emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiKeyListener.java
rename to emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiKeyListener.java
index 8bac084e..a5b2129 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiKeyListener.java
+++ b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiKeyListener.java
@@ -13,14 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.widget;
+package androidx.emoji2.helpers;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
import android.text.Editable;
+import android.text.method.KeyListener;
import android.view.KeyEvent;
import android.view.View;
+import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.emoji2.text.EmojiCompat;
@@ -34,9 +36,16 @@
@RequiresApi(19)
final class EmojiKeyListener implements android.text.method.KeyListener {
private final android.text.method.KeyListener mKeyListener;
+ private final EmojiCompatHandleKeyDownHelper mEmojiCompatHandleKeyDownHelper;
EmojiKeyListener(android.text.method.KeyListener keyListener) {
+ this(keyListener, new EmojiCompatHandleKeyDownHelper());
+ }
+
+ EmojiKeyListener(KeyListener keyListener,
+ EmojiCompatHandleKeyDownHelper emojiCompatKeydownHelper) {
mKeyListener = keyListener;
+ mEmojiCompatHandleKeyDownHelper = emojiCompatKeydownHelper;
}
@Override
@@ -46,7 +55,8 @@
@Override
public boolean onKeyDown(View view, Editable content, int keyCode, KeyEvent event) {
- final boolean result = EmojiCompat.handleOnKeyDown(content, keyCode, event);
+ final boolean result = mEmojiCompatHandleKeyDownHelper
+ .handleKeyDown(content, keyCode, event);
return result || mKeyListener.onKeyDown(view, content, keyCode, event);
}
@@ -64,4 +74,12 @@
public void clearMetaKeyState(View view, Editable content, int states) {
mKeyListener.clearMetaKeyState(view, content, states);
}
+
+ public static class EmojiCompatHandleKeyDownHelper {
+ public boolean handleKeyDown(@NonNull final Editable editable, final int keyCode,
+ @NonNull final KeyEvent event) {
+ return EmojiCompat.handleOnKeyDown(editable, keyCode, event);
+ }
+ }
+
}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTextViewHelper.java b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiTextViewHelper.java
similarity index 99%
rename from emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTextViewHelper.java
rename to emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiTextViewHelper.java
index d6a34e4..ee45e85 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTextViewHelper.java
+++ b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiTextViewHelper.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.widget;
+package androidx.emoji2.helpers;
import android.os.Build;
import android.text.InputFilter;
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTextWatcher.java b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiTextWatcher.java
similarity index 95%
rename from emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTextWatcher.java
rename to emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiTextWatcher.java
index 46fae2b..43887bb 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTextWatcher.java
+++ b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiTextWatcher.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.widget;
+package androidx.emoji2.helpers;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
@@ -26,6 +26,7 @@
import androidx.annotation.RestrictTo;
import androidx.emoji2.text.EmojiCompat;
import androidx.emoji2.text.EmojiCompat.InitCallback;
+import androidx.emoji2.text.EmojiDefaults;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
@@ -40,7 +41,7 @@
final class EmojiTextWatcher implements android.text.TextWatcher {
private final EditText mEditText;
private InitCallback mInitCallback;
- private int mMaxEmojiCount = EditTextAttributeHelper.MAX_EMOJI_COUNT;
+ private int mMaxEmojiCount = EmojiDefaults.MAX_EMOJI_COUNT;
@EmojiCompat.ReplaceStrategy
private int mEmojiReplaceStrategy = EmojiCompat.REPLACE_STRATEGY_DEFAULT;
@@ -107,6 +108,7 @@
return mInitCallback;
}
+ @RequiresApi(19)
private static class InitCallbackImpl extends InitCallback {
private final Reference<EditText> mViewRef;
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTransformationMethod.java b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiTransformationMethod.java
similarity index 98%
rename from emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTransformationMethod.java
rename to emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiTransformationMethod.java
index 16a4780..78756b6 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTransformationMethod.java
+++ b/emoji2/emoji2-views-core/src/main/java/androidx/emoji2/helpers/EmojiTransformationMethod.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.widget;
+package androidx.emoji2.helpers;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
diff --git a/emoji2/emoji2-views/AndroidManifest.xml b/emoji2/emoji2-views/AndroidManifest.xml
new file mode 100644
index 0000000..523feaf
--- /dev/null
+++ b/emoji2/emoji2-views/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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.emoji2.widget"/>
diff --git a/emoji2/emoji2-views/build.gradle b/emoji2/emoji2-views/build.gradle
new file mode 100644
index 0000000..6876fb1
--- /dev/null
+++ b/emoji2/emoji2-views/build.gradle
@@ -0,0 +1,50 @@
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.Publish
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("com.github.johnrengelman.shadow")
+}
+
+dependencies {
+ api("androidx.core:core:1.3.0-rc01")
+ api(project(":emoji2:emoji2"))
+ implementation(project(":emoji2:emoji2-views-core"))
+
+ implementation("androidx.collection:collection:1.1.0")
+
+ androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
+ androidTestImplementation(ANDROIDX_TEST_CORE)
+ androidTestImplementation(ANDROIDX_TEST_RUNNER)
+ androidTestImplementation(ANDROIDX_TEST_RULES)
+ androidTestImplementation(ESPRESSO_CORE, libs.exclude_for_espresso)
+ androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation project(':internal-testutils-runtime')
+}
+
+android {
+ sourceSets {
+ main {
+ // We use a non-standard manifest path.
+ manifest.srcFile 'AndroidManifest.xml'
+ res.srcDirs += 'src/main/res-public'
+ // TODO(seanmcq): rewrite the tests to avoid needing the font / license files
+ // removed to avoid dupe files
+ }
+ }
+}
+
+androidx {
+ name = "Android Emoji2 Compat Views"
+ publish = Publish.NONE
+ mavenVersion = LibraryVersions.EMOJI2
+ mavenGroup = LibraryGroups.EMOJI2
+ inceptionYear = "2017"
+ description = "Support for using emoji2 directly with Android Views, for use in apps without " +
+ "appcompat"
+}
diff --git a/emoji2/emoji2-views/src/androidTest/AndroidManifest.xml b/emoji2/emoji2-views/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..66a230e
--- /dev/null
+++ b/emoji2/emoji2-views/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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.emoji2.widget">
+
+ <application>
+ <activity android:name=".ViewsTestActivity"/>
+ </application>
+
+</manifest>
\ No newline at end of file
diff --git a/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/util/Emoji.java b/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/util/Emoji.java
new file mode 100644
index 0000000..73117a2
--- /dev/null
+++ b/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/util/Emoji.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.util;
+
+import androidx.annotation.NonNull;
+
+public class Emoji {
+
+ public static final int CHAR_KEYCAP = 0x20E3;
+ public static final int CHAR_DIGIT = 0x0039;
+ public static final int CHAR_ZWJ = 0x200D;
+ public static final int CHAR_VS_EMOJI = 0xFE0f;
+ public static final int CHAR_VS_TEXT = 0xFE0E;
+ public static final int CHAR_FITZPATRICK = 0x1F3FE;
+ public static final int CHAR_FITZPATRICK_TYPE_1 = 0x1F3fB;
+ public static final int CHAR_DEFAULT_TEXT_STYLE = 0x26F9;
+ public static final int CHAR_DEFAULT_EMOJI_STYLE = 0x1f3A2;
+ public static final int CHAR_FEMALE_SIGN = 0x2640;
+ public static final int CHAR_MAN = 0x1F468;
+ public static final int CHAR_HEART = 0x2764;
+ public static final int CHAR_KISS = 0x1F48B;
+ public static final int CHAR_REGIONAL_SYMBOL = 0x1F1E8;
+ public static final int CHAR_ASTERISK = 0x002A;
+
+ public static final EmojiMapping EMOJI_SINGLE_CODEPOINT = new EmojiMapping(
+ new int[]{CHAR_DEFAULT_EMOJI_STYLE}, 0xF01B4);
+
+ public static final EmojiMapping EMOJI_WITH_ZWJ = new EmojiMapping(
+ new int[]{CHAR_MAN, CHAR_ZWJ, CHAR_HEART, CHAR_VS_EMOJI, CHAR_ZWJ, CHAR_KISS, CHAR_ZWJ,
+ CHAR_MAN}, 0xF051F);
+
+ public static final EmojiMapping EMOJI_GENDER = new EmojiMapping(new int[]{
+ CHAR_DEFAULT_TEXT_STYLE, CHAR_VS_EMOJI, CHAR_ZWJ, CHAR_FEMALE_SIGN}, 0xF0950);
+
+ public static final EmojiMapping EMOJI_FLAG = new EmojiMapping(
+ new int[]{CHAR_REGIONAL_SYMBOL, CHAR_REGIONAL_SYMBOL}, 0xF03A0);
+
+ public static final EmojiMapping EMOJI_GENDER_WITHOUT_VS = new EmojiMapping(
+ new int[]{CHAR_DEFAULT_TEXT_STYLE, CHAR_ZWJ, CHAR_FEMALE_SIGN}, 0xF0950);
+
+ public static final EmojiMapping DEFAULT_TEXT_STYLE = new EmojiMapping(
+ new int[]{CHAR_DEFAULT_TEXT_STYLE, CHAR_VS_EMOJI}, 0xF04C6);
+
+ public static final EmojiMapping EMOJI_REGIONAL_SYMBOL = new EmojiMapping(
+ new int[]{CHAR_REGIONAL_SYMBOL}, 0xF0025);
+
+ public static final EmojiMapping EMOJI_UNKNOWN_FLAG = new EmojiMapping(
+ new int[]{0x1F1FA, 0x1F1F3}, 0xF0599);
+
+ public static final EmojiMapping EMOJI_DIGIT_ES = new EmojiMapping(
+ new int[]{CHAR_DIGIT, CHAR_VS_EMOJI}, 0xF0340);
+
+ public static final EmojiMapping EMOJI_DIGIT_KEYCAP = new EmojiMapping(
+ new int[]{CHAR_DIGIT, CHAR_KEYCAP}, 0xF0377);
+
+ public static final EmojiMapping EMOJI_DIGIT_ES_KEYCAP = new EmojiMapping(
+ new int[]{CHAR_DIGIT, CHAR_VS_EMOJI, CHAR_KEYCAP}, 0xF0377);
+
+ public static final EmojiMapping EMOJI_ASTERISK_KEYCAP = new EmojiMapping(
+ new int[]{CHAR_ASTERISK, CHAR_VS_EMOJI, CHAR_KEYCAP}, 0xF051D);
+
+ public static final EmojiMapping EMOJI_SKIN_MODIFIER = new EmojiMapping(
+ new int[]{CHAR_MAN, CHAR_FITZPATRICK}, 0xF0603);
+
+ public static final EmojiMapping EMOJI_SKIN_MODIFIER_TYPE_ONE = new EmojiMapping(
+ new int[]{CHAR_MAN, CHAR_FITZPATRICK_TYPE_1}, 0xF0606);
+
+ public static final EmojiMapping EMOJI_SKIN_MODIFIER_WITH_VS = new EmojiMapping(
+ new int[]{CHAR_MAN, CHAR_VS_EMOJI, CHAR_FITZPATRICK_TYPE_1}, 0xF0606);
+
+ public static class EmojiMapping {
+ private final int[] mCodepoints;
+ private final int mId;
+
+ private EmojiMapping(@NonNull final int[] codepoints, final int id) {
+ mCodepoints = codepoints;
+ mId = id;
+ }
+
+ public final int[] codepoints() {
+ return mCodepoints;
+ }
+
+ public final int id() {
+ return mId;
+ }
+
+ public final int charCount() {
+ int count = 0;
+ for (int i = 0; i < mCodepoints.length; i++) {
+ count += Character.charCount(mCodepoints[i]);
+ }
+ return count;
+ }
+ }
+}
diff --git a/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/util/EmojiMatcher.java b/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/util/EmojiMatcher.java
new file mode 100644
index 0000000..0a37ebc
--- /dev/null
+++ b/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/util/EmojiMatcher.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.emoji2.util;
+
+import static org.mockito.ArgumentMatchers.argThat;
+
+import android.text.Spanned;
+import android.text.TextUtils;
+
+import androidx.emoji2.text.EmojiSpan;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+import org.mockito.ArgumentMatcher;
+
+/**
+ * Utility class that includes matchers specific to emojis and EmojiSpans.
+ */
+public class EmojiMatcher {
+
+ public static Matcher<CharSequence> hasEmojiCount(final int count) {
+ return new EmojiCountMatcher(count);
+ }
+
+ public static <T extends CharSequence> T sameCharSequence(final T expected) {
+ return argThat(new ArgumentMatcher<T>() {
+ @Override
+ public boolean matches(T o) {
+ if (o instanceof CharSequence) {
+ return TextUtils.equals(expected, o);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "doesn't match " + expected;
+ }
+ });
+ }
+
+ private static class EmojiCountMatcher extends TypeSafeMatcher<CharSequence> {
+
+ private final int mCount;
+ private EmojiSpan[] mSpans;
+
+ EmojiCountMatcher(final int count) {
+ mCount = count;
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("should have ").appendValue(mCount).appendText(" EmojiSpans");
+ }
+
+ @Override
+ protected void describeMismatchSafely(final CharSequence charSequence,
+ Description mismatchDescription) {
+ mismatchDescription.appendText(" has ");
+ if (mSpans == null) {
+ mismatchDescription.appendValue("no");
+ } else {
+ mismatchDescription.appendValue(mSpans.length);
+ }
+
+ mismatchDescription.appendText(" EmojiSpans");
+ }
+
+ @Override
+ protected boolean matchesSafely(final CharSequence charSequence) {
+ if (charSequence == null) return false;
+ if (!(charSequence instanceof Spanned)) return false;
+ mSpans = ((Spanned) charSequence).getSpans(0, charSequence.length(), EmojiSpan.class);
+ return mSpans.length == mCount;
+ }
+ }
+
+}
diff --git a/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/util/TestString.java b/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/util/TestString.java
new file mode 100644
index 0000000..83728da
--- /dev/null
+++ b/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/util/TestString.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.emoji2.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class used to create strings with emojis during tests.
+ */
+public class TestString {
+
+ private static final List<Integer> EMPTY_LIST = new ArrayList<>();
+
+ private static final String EXTRA = "ab";
+ private final List<Integer> mCodePoints;
+ private String mString;
+ private final String mValue;
+ private boolean mHasSuffix;
+ private boolean mHasPrefix;
+
+ public TestString(int... codePoints) {
+ if (codePoints.length == 0) {
+ mCodePoints = EMPTY_LIST;
+ } else {
+ mCodePoints = new ArrayList<>();
+ append(codePoints);
+ }
+ mValue = null;
+ }
+
+ public TestString(Emoji.EmojiMapping emojiMapping) {
+ this(emojiMapping.codepoints());
+ }
+
+ public TestString(String string) {
+ mCodePoints = EMPTY_LIST;
+ mValue = string;
+ }
+
+ public TestString append(int... codePoints) {
+ for (int i = 0; i < codePoints.length; i++) {
+ mCodePoints.add(codePoints[i]);
+ }
+ return this;
+ }
+
+ public TestString prepend(int... codePoints) {
+ for (int i = codePoints.length - 1; i >= 0; i--) {
+ mCodePoints.add(0, codePoints[i]);
+ }
+ return this;
+ }
+
+ public TestString append(Emoji.EmojiMapping emojiMapping) {
+ return append(emojiMapping.codepoints());
+ }
+
+ public TestString withSuffix() {
+ mHasSuffix = true;
+ return this;
+ }
+
+ public TestString withPrefix() {
+ mHasPrefix = true;
+ return this;
+ }
+
+ @SuppressWarnings("ForLoopReplaceableByForEach")
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ if (mHasPrefix) {
+ builder.append(EXTRA);
+ }
+
+ for (int index = 0; index < mCodePoints.size(); index++) {
+ builder.append(Character.toChars(mCodePoints.get(index)));
+ }
+
+ if (mValue != null) {
+ builder.append(mValue);
+ }
+
+ if (mHasSuffix) {
+ builder.append(EXTRA);
+ }
+ mString = builder.toString();
+ return mString;
+ }
+
+ public int emojiStartIndex() {
+ if (mHasPrefix) return EXTRA.length();
+ return 0;
+ }
+
+ public int emojiEndIndex() {
+ if (mHasSuffix) return mString.lastIndexOf(EXTRA);
+ return mString.length();
+ }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextTest.java b/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextTest.java
similarity index 70%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextTest.java
rename to emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextTest.java
index 9db816f..fb58f76 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextTest.java
+++ b/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextTest.java
@@ -22,13 +22,11 @@
import android.app.Instrumentation;
-import androidx.emoji2.test.R;
import androidx.emoji2.text.EmojiCompat;
-import androidx.emoji2.text.TestActivity;
-import androidx.emoji2.text.TestConfigBuilder;
import androidx.emoji2.util.Emoji;
import androidx.emoji2.util.EmojiMatcher;
import androidx.emoji2.util.TestString;
+import androidx.emoji2.widget.test.R;
import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
@@ -37,6 +35,7 @@
import org.junit.Before;
import org.junit.BeforeClass;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -47,8 +46,8 @@
@SuppressWarnings("deprecation")
@Rule
- public androidx.test.rule.ActivityTestRule<TestActivity> mActivityRule =
- new androidx.test.rule.ActivityTestRule<>(TestActivity.class);
+ public androidx.test.rule.ActivityTestRule<ViewsTestActivity> mActivityRule =
+ new androidx.test.rule.ActivityTestRule<>(ViewsTestActivity.class);
private Instrumentation mInstrumentation;
@BeforeClass
@@ -63,7 +62,7 @@
@Test
public void testInflateWithMaxEmojiCount() {
- final TestActivity activity = mActivityRule.getActivity();
+ final ViewsTestActivity activity = mActivityRule.getActivity();
final EmojiEditText editText = activity.findViewById(R.id.editTextWithMaxCount);
// value set in XML
@@ -84,16 +83,18 @@
@Test
@UiThreadTest
public void testSetKeyListener_withNull() {
- final TestActivity activity = mActivityRule.getActivity();
+ final ViewsTestActivity activity = mActivityRule.getActivity();
final EmojiEditText editText = activity.findViewById(R.id.editTextWithMaxCount);
editText.setKeyListener(null);
assertNull(editText.getKeyListener());
}
+ //TODO(seanmcq): re-enable without dependency on font
@Test
@SdkSuppress(minSdkVersion = 19)
+ @Ignore("Disabled to avoid adding dependency on emoji font to this artifact")
public void testSetMaxCount() {
- final TestActivity activity = mActivityRule.getActivity();
+ final ViewsTestActivity activity = mActivityRule.getActivity();
final EmojiEditText editText = activity.findViewById(R.id.editTextWithMaxCount);
// set max emoji count to 1 and set text with 2 emojis
@@ -110,4 +111,25 @@
assertThat(editText.getText(), EmojiMatcher.hasEmojiCount(1));
}
+
+ //TODO(seanmcq): re-enable without dependency on font
+ @Test
+ @SdkSuppress(minSdkVersion = 19)
+ @Ignore("Disabled to avoid adding dependency on emoji font to this artifact")
+ public void testDoesReplaceEmoji() {
+ final ViewsTestActivity activity = mActivityRule.getActivity();
+ final EmojiEditText editText = activity.findViewById(R.id.editText);
+
+ mInstrumentation.runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ final String string = new TestString(Emoji.EMOJI_FLAG).append(
+ Emoji.EMOJI_FLAG).toString();
+ editText.setText(string);
+ }
+ });
+ mInstrumentation.waitForIdleSync();
+
+ assertThat(editText.getText(), EmojiMatcher.hasEmojiCount(2));
+ }
}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiExtractTextLayoutTest.java b/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/widget/EmojiExtractTextLayoutTest.java
similarity index 93%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiExtractTextLayoutTest.java
rename to emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/widget/EmojiExtractTextLayoutTest.java
index 2bb4ec04..a3144230 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiExtractTextLayoutTest.java
+++ b/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/widget/EmojiExtractTextLayoutTest.java
@@ -36,7 +36,6 @@
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
-import androidx.emoji2.R;
import androidx.emoji2.text.EmojiCompat;
import androidx.emoji2.util.EmojiMatcher;
import androidx.test.annotation.UiThreadTest;
@@ -71,7 +70,7 @@
public void testInflate() {
final Context context = ApplicationProvider.getApplicationContext();
final EmojiExtractTextLayout layout = (EmojiExtractTextLayout) LayoutInflater.from(context)
- .inflate(androidx.emoji2.test.R.layout.extract_view, null);
+ .inflate(androidx.emoji2.widget.test.R.layout.extract_view, null);
final EmojiExtractEditText extractEditText = layout.findViewById(
android.R.id.inputExtractEditText);
@@ -91,7 +90,7 @@
public void testSetKeyListener_withNull() {
final Context context = ApplicationProvider.getApplicationContext();
final EmojiExtractTextLayout layout = (EmojiExtractTextLayout) LayoutInflater.from(context)
- .inflate(androidx.emoji2.test.R.layout.extract_view, null);
+ .inflate(androidx.emoji2.widget.test.R.layout.extract_view, null);
final EmojiExtractEditText extractEditText = layout.findViewById(
android.R.id.inputExtractEditText);
@@ -107,7 +106,7 @@
final Context context = ApplicationProvider.getApplicationContext();
final EmojiExtractTextLayout layout = (EmojiExtractTextLayout) LayoutInflater.from(context)
- .inflate(androidx.emoji2.test.R.layout.extract_view_with_attrs, null);
+ .inflate(androidx.emoji2.widget.test.R.layout.extract_view_with_attrs, null);
assertEquals(EmojiCompat.REPLACE_STRATEGY_NON_EXISTENT, layout.getEmojiReplaceStrategy());
@@ -129,7 +128,7 @@
final Context context = ApplicationProvider.getApplicationContext();
final EmojiExtractTextLayout layout = (EmojiExtractTextLayout) LayoutInflater.from(context)
- .inflate(androidx.emoji2.test.R.layout.extract_view_with_attrs, null);
+ .inflate(androidx.emoji2.widget.test.R.layout.extract_view_with_attrs, null);
final EmojiExtractEditText extractEditText = layout.findViewById(
android.R.id.inputExtractEditText);
@@ -156,7 +155,7 @@
public void testOnUpdateExtractingViews() {
final Context context = ApplicationProvider.getApplicationContext();
final EmojiExtractTextLayout layout = (EmojiExtractTextLayout) LayoutInflater.from(context)
- .inflate(androidx.emoji2.test.R.layout.extract_view, null);
+ .inflate(androidx.emoji2.widget.test.R.layout.extract_view, null);
final EditorInfo editorInfo = new EditorInfo();
editorInfo.actionLabel = "My Action Label";
@@ -184,7 +183,7 @@
public void testOnUpdateExtractingViews_hidesAccessoriesIfNoAction() {
final Context context = ApplicationProvider.getApplicationContext();
final EmojiExtractTextLayout layout = (EmojiExtractTextLayout) LayoutInflater.from(context)
- .inflate(androidx.emoji2.test.R.layout.extract_view, null);
+ .inflate(androidx.emoji2.widget.test.R.layout.extract_view, null);
final EditorInfo editorInfo = new EditorInfo();
editorInfo.imeOptions = EditorInfo.IME_ACTION_NONE;
diff --git a/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/widget/EmojiTextViewTest.java b/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/widget/EmojiTextViewTest.java
new file mode 100644
index 0000000..f217a7a
--- /dev/null
+++ b/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/widget/EmojiTextViewTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.widget;
+
+import static org.junit.Assert.assertThat;
+
+import android.app.Instrumentation;
+import android.widget.TextView;
+
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.util.Emoji;
+import androidx.emoji2.util.EmojiMatcher;
+import androidx.emoji2.util.TestString;
+import androidx.emoji2.widget.test.R;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class EmojiTextViewTest {
+
+ @SuppressWarnings("deprecation")
+ @Rule
+ public androidx.test.rule.ActivityTestRule<ViewsTestActivity> mActivityRule =
+ new androidx.test.rule.ActivityTestRule<>(ViewsTestActivity.class);
+ private Instrumentation mInstrumentation;
+
+ @BeforeClass
+ public static void setupEmojiCompat() {
+ EmojiCompat.reset(TestConfigBuilder.config());
+ }
+
+ @Before
+ public void setup() {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ }
+
+ //TODO(seanmcq): re-enable without dependency on font
+ @Test
+ @SdkSuppress(minSdkVersion = 19)
+ @Ignore("Disabled to avoid adding dependency on emoji font to this artifact")
+ public void whenEmojiTextView_setText_emojiIsProcessedToSpans() {
+ final ViewsTestActivity activity = mActivityRule.getActivity();
+ final TextView textView = activity.findViewById(R.id.emojiTextView);
+
+ mInstrumentation.runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ final String string = new TestString(Emoji.EMOJI_FLAG).append(
+ Emoji.EMOJI_FLAG).toString();
+ textView.setText(string);
+ }
+ });
+ mInstrumentation.waitForIdleSync();
+
+ assertThat(textView.getText(), EmojiMatcher.hasEmojiCount(2));
+ }
+}
diff --git a/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/widget/TestConfigBuilder.java b/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/widget/TestConfigBuilder.java
new file mode 100644
index 0000000..27c77cc
--- /dev/null
+++ b/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/widget/TestConfigBuilder.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.emoji2.widget;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.MetadataRepo;
+import androidx.test.core.app.ApplicationProvider;
+
+public class TestConfigBuilder {
+ private static final String FONT_FILE = "NotoColorEmojiCompat.ttf";
+
+ private TestConfigBuilder() { }
+
+ public static EmojiCompat.Config config() {
+ return new TestConfig().setReplaceAll(true);
+ }
+
+ /**
+ * Forces the creation of Metadata instead of relying on cached metadata. If GlyphChecker is
+ * mocked, a new metadata has to be used instead of the statically cached metadata since the
+ * result of GlyphChecker on the same device might effect other tests.
+ */
+ public static EmojiCompat.Config freshConfig() {
+ return new TestConfig(new ResettingTestDataLoader()).setReplaceAll(true);
+ }
+
+ public static class TestConfig extends EmojiCompat.Config {
+ TestConfig() {
+ super(new TestEmojiDataLoader());
+ }
+
+ TestConfig(final EmojiCompat.MetadataRepoLoader metadataLoader) {
+ super(metadataLoader);
+ }
+ }
+
+ public static class TestEmojiDataLoader implements EmojiCompat.MetadataRepoLoader {
+ static final Object S_METADATA_REPO_LOCK = new Object();
+ // keep a static instance to in order not to slow down the tests
+ @GuardedBy("sMetadataRepoLock")
+ static volatile MetadataRepo sMetadataRepo;
+
+ TestEmojiDataLoader() {
+ }
+
+ @Override
+ public void load(@NonNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
+ if (sMetadataRepo == null) {
+ synchronized (S_METADATA_REPO_LOCK) {
+ if (sMetadataRepo == null) {
+ try {
+ final Context context = ApplicationProvider.getApplicationContext();
+ final AssetManager assetManager = context.getAssets();
+ sMetadataRepo = MetadataRepo.create(assetManager, FONT_FILE);
+ } catch (Throwable e) {
+ loaderCallback.onFailed(e);
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ }
+
+ loaderCallback.onLoaded(sMetadataRepo);
+ }
+ }
+
+ public static class ResettingTestDataLoader implements EmojiCompat.MetadataRepoLoader {
+ private MetadataRepo mMetadataRepo;
+
+ ResettingTestDataLoader() {
+ }
+
+ @Override
+ public void load(@NonNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
+ if (mMetadataRepo == null) {
+ try {
+ final Context context = ApplicationProvider.getApplicationContext();
+ final AssetManager assetManager = context.getAssets();
+ mMetadataRepo = MetadataRepo.create(assetManager, FONT_FILE);
+ } catch (Throwable e) {
+ loaderCallback.onFailed(e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ loaderCallback.onLoaded(mMetadataRepo);
+ }
+ }
+
+}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt b/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/widget/ViewsTestActivity.java
similarity index 62%
copy from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
copy to emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/widget/ViewsTestActivity.java
index 7e01354..5727acd 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
+++ b/emoji2/emoji2-views/src/androidTest/java/androidx/emoji2/widget/ViewsTestActivity.java
@@ -1,5 +1,3 @@
-// ktlint-disable filename
-
/*
* Copyright 2021 The Android Open Source Project
*
@@ -15,7 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package androidx.emoji2.widget;
-package androidx.compose.foundation
+import android.app.Activity;
+import android.os.Bundle;
-internal actual typealias AtomicReference<V> = java.util.concurrent.atomic.AtomicReference<V>
\ No newline at end of file
+import androidx.emoji2.widget.test.R;
+
+public class ViewsTestActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.views_activity_default);
+ }
+}
diff --git a/emoji2/emoji2/src/androidTest/res/layout/extract_view.xml b/emoji2/emoji2-views/src/androidTest/res/layout/extract_view.xml
similarity index 100%
rename from emoji2/emoji2/src/androidTest/res/layout/extract_view.xml
rename to emoji2/emoji2-views/src/androidTest/res/layout/extract_view.xml
diff --git a/emoji2/emoji2/src/androidTest/res/layout/extract_view_with_attrs.xml b/emoji2/emoji2-views/src/androidTest/res/layout/extract_view_with_attrs.xml
similarity index 100%
rename from emoji2/emoji2/src/androidTest/res/layout/extract_view_with_attrs.xml
rename to emoji2/emoji2-views/src/androidTest/res/layout/extract_view_with_attrs.xml
diff --git a/emoji2/emoji2-views/src/androidTest/res/layout/views_activity_default.xml b/emoji2/emoji2-views/src/androidTest/res/layout/views_activity_default.xml
new file mode 100644
index 0000000..b8905da
--- /dev/null
+++ b/emoji2/emoji2-views/src/androidTest/res/layout/views_activity_default.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/root"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="8sp"/>
+
+ <androidx.emoji2.widget.EmojiTextView
+ android:id="@+id/emojiTextView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="8sp" />
+
+ <androidx.emoji2.widget.EmojiEditText
+ android:id="@+id/editText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <androidx.emoji2.widget.EmojiEditText
+ android:id="@+id/editTextWithMaxCount"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:maxEmojiCount="5"/>
+
+</LinearLayout>
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EditTextAttributeHelper.java b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EditTextAttributeHelper.java
similarity index 79%
rename from emoji2/emoji2/src/main/java/androidx/emoji2/widget/EditTextAttributeHelper.java
rename to emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EditTextAttributeHelper.java
index 7157c64..1fee3e1 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EditTextAttributeHelper.java
+++ b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EditTextAttributeHelper.java
@@ -16,7 +16,7 @@
package androidx.emoji2.widget;
-import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import android.content.Context;
import android.content.res.TypedArray;
@@ -24,26 +24,27 @@
import android.view.View;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
-import androidx.emoji2.R;
+import androidx.emoji2.text.EmojiDefaults;
/**
* Helper class to parse EmojiCompat EditText attributes.
*
* @hide
*/
-@RestrictTo(LIBRARY_GROUP_PREFIX)
+@RestrictTo(LIBRARY)
public class EditTextAttributeHelper {
- static final int MAX_EMOJI_COUNT = Integer.MAX_VALUE;
private int mMaxEmojiCount;
- public EditTextAttributeHelper(@NonNull View view, AttributeSet attrs, int defStyleAttr,
- int defStyleRes) {
+ public EditTextAttributeHelper(@NonNull View view, @Nullable AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
if (attrs != null) {
final Context context = view.getContext();
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.EmojiEditText,
defStyleAttr, defStyleRes);
- mMaxEmojiCount = a.getInteger(R.styleable.EmojiEditText_maxEmojiCount, MAX_EMOJI_COUNT);
+ mMaxEmojiCount = a.getInteger(R.styleable.EmojiEditText_maxEmojiCount,
+ EmojiDefaults.MAX_EMOJI_COUNT);
a.recycle();
}
}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiButton.java b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiButton.java
similarity index 77%
rename from emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiButton.java
rename to emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiButton.java
index 3fb4089..1b20ab5 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiButton.java
+++ b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiButton.java
@@ -15,6 +15,7 @@
*/
package androidx.emoji2.widget;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.text.InputFilter;
@@ -22,8 +23,11 @@
import android.view.ActionMode;
import android.widget.Button;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.widget.TextViewCompat;
+import androidx.emoji2.helpers.EmojiTextViewHelper;
/**
* Button widget enhanced with emoji capability by using {@link EmojiTextViewHelper}. When used
@@ -38,23 +42,25 @@
*/
private boolean mInitialized;
- public EmojiButton(Context context) {
+ public EmojiButton(@NonNull Context context) {
super(context);
init();
}
- public EmojiButton(Context context, AttributeSet attrs) {
+ public EmojiButton(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
- public EmojiButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ public EmojiButton(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
+ @SuppressLint("UnsafeNewApiCall")
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
- public EmojiButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ public EmojiButton(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
@@ -67,7 +73,7 @@
}
@Override
- public void setFilters(InputFilter[] filters) {
+ public void setFilters(@NonNull InputFilter[] filters) {
super.setFilters(getEmojiTextViewHelper().getFilters(filters));
}
@@ -89,7 +95,9 @@
* {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
*/
@Override
- public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+ public void setCustomSelectionActionModeCallback(
+ @NonNull ActionMode.Callback actionModeCallback
+ ) {
super.setCustomSelectionActionModeCallback(TextViewCompat
.wrapCustomSelectionActionModeCallback(this, actionModeCallback));
}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiEditText.java b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiEditText.java
similarity index 85%
rename from emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiEditText.java
rename to emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiEditText.java
index a97688b..1dae701 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiEditText.java
+++ b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiEditText.java
@@ -15,6 +15,7 @@
*/
package androidx.emoji2.widget;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.text.method.KeyListener;
@@ -25,9 +26,11 @@
import android.widget.EditText;
import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.widget.TextViewCompat;
+import androidx.emoji2.helpers.EmojiEditTextHelper;
import androidx.emoji2.text.EmojiCompat;
/**
@@ -45,23 +48,25 @@
*/
private boolean mInitialized;
- public EmojiEditText(Context context) {
+ public EmojiEditText(@NonNull Context context) {
super(context);
init(null /*attrs*/, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
}
- public EmojiEditText(Context context, AttributeSet attrs) {
+ public EmojiEditText(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(attrs, android.R.attr.editTextStyle, 0 /*defStyleRes*/);
}
- public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr) {
+ public EmojiEditText(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs, defStyleAttr, 0 /*defStyleRes*/);
}
+ @SuppressLint("UnsafeNewApiCall")
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
- public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ public EmojiEditText(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(attrs, defStyleAttr, defStyleRes);
}
@@ -84,8 +89,9 @@
super.setKeyListener(keyListener);
}
+ @Nullable
@Override
- public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) {
final InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
return getEmojiEditTextHelper().onCreateInputConnection(inputConnection, outAttrs);
}
@@ -130,7 +136,9 @@
* {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
*/
@Override
- public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+ public void setCustomSelectionActionModeCallback(
+ @NonNull ActionMode.Callback actionModeCallback
+ ) {
super.setCustomSelectionActionModeCallback(TextViewCompat
.wrapCustomSelectionActionModeCallback(this, actionModeCallback));
}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java
similarity index 87%
rename from emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java
rename to emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java
index 288d3d9..95df225 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java
+++ b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java
@@ -18,6 +18,7 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.inputmethodservice.ExtractEditText;
import android.os.Build;
@@ -29,10 +30,12 @@
import android.widget.TextView;
import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.widget.TextViewCompat;
+import androidx.emoji2.helpers.EmojiEditTextHelper;
import androidx.emoji2.text.EmojiCompat;
import androidx.emoji2.text.EmojiSpan;
@@ -53,24 +56,26 @@
*/
private boolean mInitialized;
- public EmojiExtractEditText(Context context) {
+ public EmojiExtractEditText(@NonNull Context context) {
super(context);
init(null /*attrs*/, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
}
- public EmojiExtractEditText(Context context, AttributeSet attrs) {
+ public EmojiExtractEditText(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(attrs, android.R.attr.editTextStyle, 0 /*defStyleRes*/);
}
- public EmojiExtractEditText(Context context, AttributeSet attrs, int defStyleAttr) {
+ public EmojiExtractEditText(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs, defStyleAttr, 0 /*defStyleRes*/);
}
+ @SuppressLint("UnsafeNewApiCall")
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
- public EmojiExtractEditText(Context context, AttributeSet attrs, int defStyleAttr,
- int defStyleRes) {
+ public EmojiExtractEditText(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(attrs, defStyleAttr, defStyleRes);
}
@@ -93,8 +98,9 @@
super.setKeyListener(keyListener);
}
+ @Nullable
@Override
- public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) {
final InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
return getEmojiEditTextHelper().onCreateInputConnection(inputConnection, outAttrs);
}
@@ -158,7 +164,9 @@
* {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
*/
@Override
- public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+ public void setCustomSelectionActionModeCallback(
+ @NonNull ActionMode.Callback actionModeCallback
+ ) {
super.setCustomSelectionActionModeCallback(TextViewCompat
.wrapCustomSelectionActionModeCallback(this, actionModeCallback));
}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java
similarity index 94%
rename from emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java
rename to emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java
index ba476e1..c930389 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java
+++ b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java
@@ -16,6 +16,7 @@
package androidx.emoji2.widget;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.inputmethodservice.InputMethodService;
@@ -33,7 +34,6 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.view.ViewCompat;
-import androidx.emoji2.R;
import androidx.emoji2.text.EmojiCompat;
import androidx.emoji2.text.EmojiSpan;
@@ -76,25 +76,26 @@
*/
private boolean mInitialized;
- public EmojiExtractTextLayout(Context context) {
+ public EmojiExtractTextLayout(@NonNull Context context) {
super(context);
init(context, null /*attrs*/, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
}
- public EmojiExtractTextLayout(Context context,
+ public EmojiExtractTextLayout(@NonNull Context context,
@Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
}
- public EmojiExtractTextLayout(Context context,
+ public EmojiExtractTextLayout(@NonNull Context context,
@Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr, 0 /*defStyleRes*/);
}
+ @SuppressLint("UnsafeNewApiCall")
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
- public EmojiExtractTextLayout(Context context, AttributeSet attrs,
+ public EmojiExtractTextLayout(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs, defStyleAttr, defStyleRes);
@@ -160,7 +161,8 @@
* {@link InputMethodService#onUpdateExtractingViews(EditorInfo)
* InputMethodService#onUpdateExtractingViews(EditorInfo)}.
*/
- public void onUpdateExtractingViews(InputMethodService inputMethodService, EditorInfo ei) {
+ public void onUpdateExtractingViews(@NonNull InputMethodService inputMethodService,
+ @NonNull EditorInfo ei) {
// the following code is ported as it is from InputMethodService.onUpdateExtractingViews
if (!inputMethodService.isExtractViewShown()) {
return;
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTextView.java b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiTextView.java
similarity index 77%
rename from emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTextView.java
rename to emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiTextView.java
index a9a7492..709ddd3 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTextView.java
+++ b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/EmojiTextView.java
@@ -15,6 +15,7 @@
*/
package androidx.emoji2.widget;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.text.InputFilter;
@@ -22,8 +23,11 @@
import android.view.ActionMode;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.widget.TextViewCompat;
+import androidx.emoji2.helpers.EmojiTextViewHelper;
/**
* TextView widget enhanced with emoji capability by using {@link EmojiTextViewHelper}. When used
@@ -38,23 +42,25 @@
*/
private boolean mInitialized;
- public EmojiTextView(Context context) {
+ public EmojiTextView(@NonNull Context context) {
super(context);
init();
}
- public EmojiTextView(Context context, AttributeSet attrs) {
+ public EmojiTextView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
- public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+ public EmojiTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
+ @SuppressLint("UnsafeNewApiCall")
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
- public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ public EmojiTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
@@ -67,7 +73,7 @@
}
@Override
- public void setFilters(InputFilter[] filters) {
+ public void setFilters(@NonNull InputFilter[] filters) {
super.setFilters(getEmojiTextViewHelper().getFilters(filters));
}
@@ -89,7 +95,9 @@
* {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
*/
@Override
- public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+ public void setCustomSelectionActionModeCallback(
+ @NonNull ActionMode.Callback actionModeCallback
+ ) {
super.setCustomSelectionActionModeCallback(TextViewCompat
.wrapCustomSelectionActionModeCallback(this, actionModeCallback));
}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java
similarity index 72%
rename from emoji2/emoji2/src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java
rename to emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java
index 26070ab..cf839e0 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java
+++ b/emoji2/emoji2-views/src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java
@@ -18,12 +18,15 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.ActionMode;
import android.widget.Button;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.widget.TextViewCompat;
@@ -35,21 +38,23 @@
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public class ExtractButtonCompat extends Button {
- public ExtractButtonCompat(Context context) {
+ public ExtractButtonCompat(@NonNull Context context) {
super(context, null);
}
- public ExtractButtonCompat(Context context, AttributeSet attrs) {
+ public ExtractButtonCompat(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
- public ExtractButtonCompat(Context context, AttributeSet attrs, int defStyleAttr) {
+ public ExtractButtonCompat(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
+ @SuppressLint("UnsafeNewApiCall")
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
- public ExtractButtonCompat(Context context, AttributeSet attrs, int defStyleAttr,
- int defStyleRes) {
+ public ExtractButtonCompat(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@@ -59,7 +64,7 @@
*/
@Override
public boolean hasWindowFocus() {
- return isEnabled() && getVisibility() == VISIBLE ? true : false;
+ return isEnabled() && getVisibility() == VISIBLE;
}
/**
@@ -67,7 +72,9 @@
* {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
*/
@Override
- public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+ public void setCustomSelectionActionModeCallback(
+ @NonNull ActionMode.Callback actionModeCallback
+ ) {
super.setCustomSelectionActionModeCallback(TextViewCompat
.wrapCustomSelectionActionModeCallback(this, actionModeCallback));
}
diff --git a/emoji2/emoji2/src/main/res-public/values/public_attrs.xml b/emoji2/emoji2-views/src/main/res-public/values/public_attrs.xml
similarity index 100%
rename from emoji2/emoji2/src/main/res-public/values/public_attrs.xml
rename to emoji2/emoji2-views/src/main/res-public/values/public_attrs.xml
diff --git a/emoji2/emoji2/src/main/res/layout/input_method_extract_view.xml b/emoji2/emoji2-views/src/main/res/layout/input_method_extract_view.xml
similarity index 62%
rename from emoji2/emoji2/src/main/res/layout/input_method_extract_view.xml
rename to emoji2/emoji2-views/src/main/res/layout/input_method_extract_view.xml
index 0a4d9b0..c7f18b8 100644
--- a/emoji2/emoji2/src/main/res/layout/input_method_extract_view.xml
+++ b/emoji2/emoji2-views/src/main/res/layout/input_method_extract_view.xml
@@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2017 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
-->
<merge
diff --git a/emoji2/emoji2/src/main/res/values/attrs.xml b/emoji2/emoji2-views/src/main/res/values/attrs.xml
similarity index 100%
rename from emoji2/emoji2/src/main/res/values/attrs.xml
rename to emoji2/emoji2-views/src/main/res/values/attrs.xml
diff --git a/emoji2/emoji2/build.gradle b/emoji2/emoji2/build.gradle
index ec4a8ea..b85383f 100644
--- a/emoji2/emoji2/build.gradle
+++ b/emoji2/emoji2/build.gradle
@@ -43,7 +43,6 @@
main {
// We use a non-standard manifest path.
manifest.srcFile 'AndroidManifest.xml'
- res.srcDirs += 'src/main/res-public'
resources {
srcDirs += [fontDir.getAbsolutePath()]
includes += ["LICENSE_UNICODE", "LICENSE_OFL"]
diff --git a/emoji2/emoji2/lint-baseline.xml b/emoji2/emoji2/lint-baseline.xml
deleted file mode 100644
index d6d91ea..0000000
--- a/emoji2/emoji2/lint-baseline.xml
+++ /dev/null
@@ -1,1416 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
-
- <issue
- id="PrivateConstructorForUtilityClass"
- message="Utility class with non private constructor"
- errorLine1=" private static final class CodepointIndexFinder {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiProcessor.java"
- line="654"
- column="32"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 21, the call containing class androidx.emoji2.widget.EmojiButton is not annotated with @RequiresApi(x) where x is at least 21. Either annotate the containing class with at least @RequiresApi(21) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(21)."
- errorLine1=" super(context, attrs, defStyleAttr, defStyleRes);"
- errorLine2=" ~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
- line="58"
- column="9"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 21, the call containing class androidx.emoji2.widget.EmojiEditText is not annotated with @RequiresApi(x) where x is at least 21. Either annotate the containing class with at least @RequiresApi(21) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(21)."
- errorLine1=" super(context, attrs, defStyleAttr, defStyleRes);"
- errorLine2=" ~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
- line="65"
- column="9"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 21, the call containing class androidx.emoji2.widget.EmojiExtractEditText is not annotated with @RequiresApi(x) where x is at least 21. Either annotate the containing class with at least @RequiresApi(21) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(21)."
- errorLine1=" super(context, attrs, defStyleAttr, defStyleRes);"
- errorLine2=" ~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
- line="74"
- column="9"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 21, the call containing class androidx.emoji2.widget.EmojiExtractTextLayout is not annotated with @RequiresApi(x) where x is at least 21. Either annotate the containing class with at least @RequiresApi(21) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(21)."
- errorLine1=" super(context, attrs, defStyleAttr, defStyleRes);"
- errorLine2=" ~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
- line="99"
- column="9"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 19, the call containing class androidx.emoji2.widget.EmojiInputFilter.InitCallbackImpl is not annotated with @RequiresApi(x) where x is at least 19. Either annotate the containing class with at least @RequiresApi(19) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(19)."
- errorLine1=" if (textView != null && textView.isAttachedToWindow()) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiInputFilter.java"
- line="110"
- column="46"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 19, the call containing class androidx.emoji2.text.EmojiProcessor.CodepointIndexFinder is not annotated with @RequiresApi(x) where x is at least 19. Either annotate the containing class with at least @RequiresApi(19) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(19)."
- errorLine1=" if (!Character.isSurrogate(c)) {"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiProcessor.java"
- line="702"
- column="32"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 19, the call containing class androidx.emoji2.text.EmojiProcessor.CodepointIndexFinder is not annotated with @RequiresApi(x) where x is at least 19. Either annotate the containing class with at least @RequiresApi(19) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(19)."
- errorLine1=" if (!Character.isSurrogate(c)) {"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiProcessor.java"
- line="759"
- column="32"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 21, the call containing class androidx.emoji2.widget.EmojiTextView is not annotated with @RequiresApi(x) where x is at least 21. Either annotate the containing class with at least @RequiresApi(21) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(21)."
- errorLine1=" super(context, attrs, defStyleAttr, defStyleRes);"
- errorLine2=" ~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
- line="58"
- column="9"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 19, the call containing class androidx.emoji2.widget.EmojiTextWatcher.InitCallbackImpl is not annotated with @RequiresApi(x) where x is at least 19. Either annotate the containing class with at least @RequiresApi(19) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(19)."
- errorLine1=" if (editText != null && editText.isAttachedToWindow()) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiTextWatcher.java"
- line="121"
- column="46"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 21, the call containing class androidx.emoji2.widget.ExtractButtonCompat is not annotated with @RequiresApi(x) where x is at least 21. Either annotate the containing class with at least @RequiresApi(21) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(21)."
- errorLine1=" super(context, attrs, defStyleAttr, defStyleRes);"
- errorLine2=" ~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
- line="53"
- column="9"/>
- </issue>
-
- <issue
- id="KotlinPropertyAccess"
- message="The getter return type (`int`) and setter parameter type (`boolean`) getter and setter methods for property `hasGlyph` should have exactly the same type to allow be accessed as a property from Kotlin; see https://android.github.io/kotlin-guides/interop.html#property-prefixes"
- errorLine1=" public int getHasGlyph() {"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiMetadata.java"
- line="184"
- column="16"/>
- <location
- file="src/main/java/androidx/emoji2/text/EmojiMetadata.java"
- line="193"
- column="17"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EditTextAttributeHelper(@NonNull View view, AttributeSet attrs, int defStyleAttr,"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EditTextAttributeHelper.java"
- line="40"
- column="56"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiButton(Context context) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
- line="41"
- column="24"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiButton(Context context, AttributeSet attrs) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
- line="46"
- column="24"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiButton(Context context, AttributeSet attrs) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
- line="46"
- column="41"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiButton(Context context, AttributeSet attrs, int defStyleAttr) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
- line="51"
- column="24"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiButton(Context context, AttributeSet attrs, int defStyleAttr) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
- line="51"
- column="41"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
- line="57"
- column="24"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
- line="57"
- column="41"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public void setFilters(InputFilter[] filters) {"
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
- line="70"
- column="28"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
- line="92"
- column="54"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public static EmojiCompat init(@NonNull final Config config) {"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
- line="302"
- column="19"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public static EmojiCompat reset(@NonNull final Config config) {"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
- line="322"
- column="19"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public static EmojiCompat reset(final EmojiCompat emojiCompat) {"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
- line="337"
- column="19"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public static EmojiCompat reset(final EmojiCompat emojiCompat) {"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
- line="337"
- column="43"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public static EmojiCompat get() {"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
- line="352"
- column="19"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" final KeyEvent event) {"
- errorLine2=" ~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
- line="541"
- column="19"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public CharSequence process(@NonNull final CharSequence charSequence) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
- line="625"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public CharSequence process(@NonNull final CharSequence charSequence,"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
- line="662"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public CharSequence process(@NonNull final CharSequence charSequence,"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
- line="698"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public CharSequence process(@NonNull final CharSequence charSequence,"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
- line="739"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public Config registerInitCallback(@NonNull InitCallback initCallback) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
- line="982"
- column="16"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public Config unregisterInitCallback(@NonNull InitCallback initCallback) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
- line="1000"
- column="16"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public Config setReplaceAll(final boolean replaceAll) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
- line="1017"
- column="16"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public Config setUseEmojiAsDefaultStyle(final boolean useEmojiAsDefaultStyle) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
- line="1037"
- column="16"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public Config setUseEmojiAsDefaultStyle(final boolean useEmojiAsDefaultStyle,"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
- line="1057"
- column="16"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public Config setEmojiSpanIndicatorEnabled(boolean emojiSpanIndicatorEnabled) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
- line="1081"
- column="16"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public Config setEmojiSpanIndicatorColor(@ColorInt int color) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
- line="1092"
- column="16"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public Config setMetadataLoadStrategy(@LoadStrategy int strategy) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
- line="1133"
- column="16"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" protected final MetadataRepoLoader getMetadataRepoLoader() {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
- line="1154"
- column="25"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiEditText(Context context) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
- line="48"
- column="26"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiEditText(Context context, AttributeSet attrs) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
- line="53"
- column="26"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiEditText(Context context, AttributeSet attrs) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
- line="53"
- column="43"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
- line="58"
- column="26"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
- line="58"
- column="43"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
- line="64"
- column="26"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
- line="64"
- column="43"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public InputConnection onCreateInputConnection(EditorInfo outAttrs) {"
- errorLine2=" ~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
- line="88"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public InputConnection onCreateInputConnection(EditorInfo outAttrs) {"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
- line="88"
- column="52"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
- line="133"
- column="54"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiExtractEditText(Context context) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
- line="56"
- column="33"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiExtractEditText(Context context, AttributeSet attrs) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
- line="61"
- column="33"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiExtractEditText(Context context, AttributeSet attrs) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
- line="61"
- column="50"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiExtractEditText(Context context, AttributeSet attrs, int defStyleAttr) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
- line="66"
- column="33"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiExtractEditText(Context context, AttributeSet attrs, int defStyleAttr) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
- line="66"
- column="50"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiExtractEditText(Context context, AttributeSet attrs, int defStyleAttr,"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
- line="72"
- column="33"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiExtractEditText(Context context, AttributeSet attrs, int defStyleAttr,"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
- line="72"
- column="50"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public InputConnection onCreateInputConnection(EditorInfo outAttrs) {"
- errorLine2=" ~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
- line="97"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public InputConnection onCreateInputConnection(EditorInfo outAttrs) {"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
- line="97"
- column="52"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
- line="161"
- column="54"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiExtractTextLayout(Context context) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
- line="79"
- column="35"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiExtractTextLayout(Context context,"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
- line="84"
- column="35"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiExtractTextLayout(Context context,"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
- line="90"
- column="35"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiExtractTextLayout(Context context, AttributeSet attrs,"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
- line="97"
- column="35"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiExtractTextLayout(Context context, AttributeSet attrs,"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
- line="97"
- column="52"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public void onUpdateExtractingViews(InputMethodService inputMethodService, EditorInfo ei) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
- line="163"
- column="41"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public void onUpdateExtractingViews(InputMethodService inputMethodService, EditorInfo ei) {"
- errorLine2=" ~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
- line="163"
- column="80"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public Typeface getTypeface() {"
- errorLine2=" ~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiMetadata.java"
- line="120"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public int getSize(@NonNull final Paint paint, final CharSequence text, final int start,"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiSpan.java"
- line="77"
- column="58"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" final int end, final Paint.FontMetricsInt fm) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/EmojiSpan.java"
- line="78"
- column="34"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiTextView(Context context) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
- line="41"
- column="26"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiTextView(Context context, AttributeSet attrs) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
- line="46"
- column="26"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiTextView(Context context, AttributeSet attrs) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
- line="46"
- column="43"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
- line="51"
- column="26"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
- line="51"
- column="43"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
- line="57"
- column="26"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
- line="57"
- column="43"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public void setFilters(InputFilter[] filters) {"
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
- line="70"
- column="28"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
- line="92"
- column="54"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public ExtractButtonCompat(Context context) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
- line="38"
- column="32"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public ExtractButtonCompat(Context context, AttributeSet attrs) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
- line="42"
- column="32"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public ExtractButtonCompat(Context context, AttributeSet attrs) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
- line="42"
- column="49"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public ExtractButtonCompat(Context context, AttributeSet attrs, int defStyleAttr) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
- line="46"
- column="32"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public ExtractButtonCompat(Context context, AttributeSet attrs, int defStyleAttr) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
- line="46"
- column="49"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public ExtractButtonCompat(Context context, AttributeSet attrs, int defStyleAttr,"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
- line="51"
- column="32"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public ExtractButtonCompat(Context context, AttributeSet attrs, int defStyleAttr,"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
- line="51"
- column="49"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
- line="70"
- column="54"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public FontRequestEmojiCompatConfig setHandler(Handler handler) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java"
- line="143"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public FontRequestEmojiCompatConfig setHandler(Handler handler) {"
- errorLine2=" ~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java"
- line="143"
- column="52"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public FontRequestEmojiCompatConfig setRetryPolicy(RetryPolicy policy) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java"
- line="156"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public FontRequestEmojiCompatConfig setRetryPolicy(RetryPolicy policy) {"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java"
- line="156"
- column="56"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public FontFamilyResult fetchFonts(@NonNull Context context,"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java"
- line="335"
- column="16"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public Typeface buildTypeface(@NonNull Context context,"
- errorLine2=" ~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java"
- line="341"
- column="16"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public static MetadataRepo create(@NonNull final Typeface typeface,"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/MetadataRepo.java"
- line="103"
- column="19"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public static MetadataRepo create(@NonNull final Typeface typeface,"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/MetadataRepo.java"
- line="115"
- column="19"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public static MetadataRepo create(@NonNull final AssetManager assetManager,"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/MetadataRepo.java"
- line="127"
- column="19"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" final String assetPath) throws IOException {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/MetadataRepo.java"
- line="128"
- column="19"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public char[] getEmojiCharArray() {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/MetadataRepo.java"
- line="176"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public MetadataList getMetadataList() {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/MetadataRepo.java"
- line="184"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public CharSequence subSequence(int start, int end) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="124"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public void setSpan(Object what, int start, int end, int flags) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="134"
- column="25"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) {"
- errorLine2=" ~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="149"
- column="16"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) {"
- errorLine2=" ~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="149"
- column="59"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public void removeSpan(Object what) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="167"
- column="28"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public int getSpanStart(Object tag) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="189"
- column="29"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public int getSpanEnd(Object tag) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="203"
- column="27"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public int getSpanFlags(Object tag) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="217"
- column="29"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public int nextSpanTransition(int start, int limit, Class type) {"
- errorLine2=" ~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="231"
- column="57"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public SpannableStringBuilder replace(int start, int end, CharSequence tb) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="301"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public SpannableStringBuilder replace(int start, int end, CharSequence tb) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="301"
- column="63"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart,"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="309"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart,"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="309"
- column="63"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public SpannableStringBuilder insert(int where, CharSequence tb) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="318"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public SpannableStringBuilder insert(int where, CharSequence tb) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="318"
- column="53"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="324"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="324"
- column="53"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public SpannableStringBuilder delete(int start, int end) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="330"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public SpannableStringBuilder append(CharSequence text) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="336"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public SpannableStringBuilder append(CharSequence text) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="336"
- column="42"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public SpannableStringBuilder append(char text) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="342"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public SpannableStringBuilder append(CharSequence text, int start, int end) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="348"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public SpannableStringBuilder append(CharSequence text, int start, int end) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="348"
- column="42"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public SpannableStringBuilder append(CharSequence text, Object what, int flags) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="354"
- column="12"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public SpannableStringBuilder append(CharSequence text, Object what, int flags) {"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="354"
- column="42"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public SpannableStringBuilder append(CharSequence text, Object what, int flags) {"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
- line="354"
- column="61"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public TypefaceEmojiSpan(final EmojiMetadata metadata) {"
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/TypefaceEmojiSpan.java"
- line="48"
- column="36"/>
- </issue>
-
- <issue
- id="UnknownNullness"
- message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
- errorLine1=" public void draw(@NonNull final Canvas canvas, final CharSequence text,"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/emoji2/text/TypefaceEmojiSpan.java"
- line="53"
- column="58"/>
- </issue>
-
-</issues>
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/MetadataRepoTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/MetadataRepoTest.java
index cead30d..3039f17 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/MetadataRepoTest.java
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/MetadataRepoTest.java
@@ -15,12 +15,16 @@
*/
package androidx.emoji2.text;
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.mock;
+
+import android.graphics.Typeface;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
+import androidx.text.emoji.flatbuffer.MetadataList;
import org.junit.Before;
import org.junit.Test;
@@ -35,7 +39,7 @@
@Before
public void clearResourceIndex() {
- mMetadataRepo = new MetadataRepo();
+ mMetadataRepo = MetadataRepo.create(mock(Typeface.class), new MetadataList());
}
@Test(expected = NullPointerException.class)
@@ -63,10 +67,10 @@
mMetadataRepo.put(metadata);
assertSame(metadata, getNode(codePoint));
- assertEquals(null, getNode(new int[]{1}));
- assertEquals(null, getNode(new int[]{1, 2}));
- assertEquals(null, getNode(new int[]{1, 2, 3}));
- assertEquals(null, getNode(new int[]{1, 2, 3, 5}));
+ assertNull(getNode(new int[]{1}));
+ assertNull(getNode(new int[]{1, 2}));
+ assertNull(getNode(new int[]{1, 2, 3}));
+ assertNull(getNode(new int[]{1, 2, 3, 5}));
}
@Test
@@ -88,8 +92,8 @@
assertSame(metadata2, getNode(codePoint2));
assertSame(metadata3, getNode(codePoint3));
- assertEquals(null, getNode(new int[]{1}));
- assertEquals(null, getNode(new int[]{1, 2, 3, 4, 5}));
+ assertNull(getNode(new int[]{1}));
+ assertNull(getNode(new int[]{1, 2, 3, 4, 5}));
}
final EmojiMetadata getNode(final int[] codepoints) {
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/SpannableBuilderTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/SpannableBuilderTest.java
similarity index 98%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/SpannableBuilderTest.java
rename to emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/SpannableBuilderTest.java
index 919ba54..bbbbe48 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/SpannableBuilderTest.java
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/SpannableBuilderTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.widget;
+package androidx.emoji2.text;
import static org.hamcrest.Matchers.arrayWithSize;
import static org.hamcrest.Matchers.instanceOf;
@@ -39,7 +39,6 @@
import android.text.TextWatcher;
import android.text.style.QuoteSpan;
-import androidx.emoji2.text.EmojiSpan;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestConfigBuilder.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestConfigBuilder.java
index 6753840..e44b5a4 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestConfigBuilder.java
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestConfigBuilder.java
@@ -16,13 +16,16 @@
package androidx.emoji2.text;
import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
import android.content.Context;
import android.content.res.AssetManager;
+import android.graphics.Typeface;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
+import androidx.text.emoji.flatbuffer.MetadataList;
import java.util.concurrent.CountDownLatch;
@@ -85,7 +88,8 @@
try {
mLoaderLatch.await();
if (mSuccess) {
- loaderCallback.onLoaded(new MetadataRepo());
+ loaderCallback.onLoaded(MetadataRepo.create(mock(Typeface.class),
+ new MetadataList()));
} else {
loaderCallback.onFailed(null);
}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiInputConnectionTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiInputConnectionTest.java
deleted file mode 100644
index 11d580a..0000000
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiInputConnectionTest.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.emoji2.widget;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.os.Build;
-import android.text.Editable;
-import android.text.Selection;
-import android.text.SpannableStringBuilder;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
-import android.widget.TextView;
-
-import androidx.emoji2.text.EmojiCompat;
-import androidx.emoji2.text.TestConfigBuilder;
-import androidx.emoji2.util.Emoji;
-import androidx.emoji2.util.EmojiMatcher;
-import androidx.emoji2.util.TestString;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.hamcrest.MatcherAssert;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = 19)
-public class EmojiInputConnectionTest {
-
- private InputConnection mInputConnection;
- private TestString mTestString;
- private Editable mEditable;
- private EmojiInputConnection mEmojiEmojiInputConnection;
-
- @BeforeClass
- public static void setupEmojiCompat() {
- EmojiCompat.reset(TestConfigBuilder.config());
- }
-
- @Before
- public void setup() {
- mTestString = new TestString(Emoji.EMOJI_WITH_ZWJ).withPrefix().withSuffix();
- mEditable = new SpannableStringBuilder(mTestString.toString());
- mInputConnection = mock(InputConnection.class);
- final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
- final TextView textView = spy(new TextView(context));
- EmojiCompat.get().process(mEditable);
- MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji());
-
- doReturn(mEditable).when(textView).getEditableText();
- when(mInputConnection.deleteSurroundingText(anyInt(), anyInt())).thenReturn(false);
- setupDeleteSurroundingText();
-
- mEmojiEmojiInputConnection = new EmojiInputConnection(textView, mInputConnection,
- new EditorInfo());
- }
-
- private void setupDeleteSurroundingText() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- when(mInputConnection.deleteSurroundingTextInCodePoints(anyInt(), anyInt())).thenReturn(
- false);
- }
- }
-
- @Test
- public void testDeleteSurroundingText_doesNotDelete() {
- Selection.setSelection(mEditable, 0, mEditable.length());
- assertFalse(mEmojiEmojiInputConnection.deleteSurroundingText(1, 0));
- verify(mInputConnection, times(1)).deleteSurroundingText(1, 0);
- }
-
- @Test
- public void testDeleteSurroundingText_deletesEmojiBackward() {
- Selection.setSelection(mEditable, mTestString.emojiEndIndex());
- assertTrue(mEmojiEmojiInputConnection.deleteSurroundingText(1, 0));
- verify(mInputConnection, never()).deleteSurroundingText(anyInt(), anyInt());
- }
-
- @Test
- public void testDeleteSurroundingText_doesNotDeleteEmojiIfSelectionAtStartIndex() {
- Selection.setSelection(mEditable, mTestString.emojiStartIndex());
- assertFalse(mEmojiEmojiInputConnection.deleteSurroundingText(1, 0));
- verify(mInputConnection, times(1)).deleteSurroundingText(1, 0);
- }
-
- @Test
- public void testDeleteSurroundingText_deletesEmojiForward() {
- Selection.setSelection(mEditable, mTestString.emojiStartIndex());
- assertTrue(mEmojiEmojiInputConnection.deleteSurroundingText(0, 1));
- verify(mInputConnection, never()).deleteSurroundingText(anyInt(), anyInt());
- }
-
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
- @Test
- public void testDeleteSurroundingTextInCodePoints_doesNotDelete() {
- Selection.setSelection(mEditable, 0, mEditable.length());
- assertFalse(mEmojiEmojiInputConnection.deleteSurroundingTextInCodePoints(1, 0));
- verify(mInputConnection, times(1)).deleteSurroundingTextInCodePoints(1, 0);
- }
-
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
- @Test
- public void testDeleteSurroundingTextInCodePoints_deletesEmojiBackward() {
- Selection.setSelection(mEditable, mTestString.emojiEndIndex());
- assertTrue(mEmojiEmojiInputConnection.deleteSurroundingTextInCodePoints(1, 0));
- verify(mInputConnection, never()).deleteSurroundingTextInCodePoints(anyInt(), anyInt());
- }
-
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
- @Test
- public void testDeleteSurroundingTextInCodePoints_deletesEmojiForward() {
- Selection.setSelection(mEditable, mTestString.emojiStartIndex());
- assertTrue(mEmojiEmojiInputConnection.deleteSurroundingTextInCodePoints(0, 1));
- verify(mInputConnection, never()).deleteSurroundingTextInCodePoints(anyInt(), anyInt());
- }
-
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
- @Test
- public void testDeleteSurroundingTextInCodePoints_doesNotDeleteEmojiIfSelectionAtStartIndex() {
- Selection.setSelection(mEditable, mTestString.emojiStartIndex());
- assertFalse(mEmojiEmojiInputConnection.deleteSurroundingTextInCodePoints(1, 0));
- verify(mInputConnection, times(1)).deleteSurroundingTextInCodePoints(1, 0);
- }
-}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiKeyListenerTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiKeyListenerTest.java
deleted file mode 100644
index 3bee8fe..0000000
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiKeyListenerTest.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.emoji2.widget;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.same;
-import static org.mockito.Mockito.mock;
-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.text.Editable;
-import android.text.Selection;
-import android.text.SpannableStringBuilder;
-import android.text.method.KeyListener;
-import android.view.KeyEvent;
-import android.view.View;
-
-import androidx.emoji2.text.EmojiCompat;
-import androidx.emoji2.text.TestConfigBuilder;
-import androidx.emoji2.util.Emoji;
-import androidx.emoji2.util.EmojiMatcher;
-import androidx.emoji2.util.KeyboardUtil;
-import androidx.emoji2.util.TestString;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.filters.SmallTest;
-
-import org.hamcrest.MatcherAssert;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = 19)
-public class EmojiKeyListenerTest {
-
- private KeyListener mKeyListener;
- private TestString mTestString;
- private Editable mEditable;
- private EmojiKeyListener mEmojiKeyListener;
-
- @BeforeClass
- public static void setupEmojiCompat() {
- EmojiCompat.reset(TestConfigBuilder.config());
- }
-
- @Before
- public void setup() {
- mKeyListener = mock(KeyListener.class);
- mTestString = new TestString(Emoji.EMOJI_WITH_ZWJ).withPrefix().withSuffix();
- mEditable = new SpannableStringBuilder(mTestString.toString());
- mEmojiKeyListener = new EmojiKeyListener(mKeyListener);
- EmojiCompat.get().process(mEditable);
- MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji());
-
- when(mKeyListener.onKeyDown(any(View.class), any(Editable.class), anyInt(),
- any(KeyEvent.class))).thenReturn(false);
- }
-
- @Test
- public void testOnKeyDown_doesNotDelete_whenKeyCodeIsNotDelOrForwardDel() {
- Selection.setSelection(mEditable, 0);
- final KeyEvent event = KeyboardUtil.zero();
- assertFalse(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
- verify(mKeyListener, times(1)).onKeyDown((View) eq(null), same(mEditable),
- eq(event.getKeyCode()), same(event));
- }
-
- @Test
- public void testOnKeyDown_doesNotDelete_withOtherModifiers() {
- Selection.setSelection(mEditable, 0);
- final KeyEvent event = KeyboardUtil.fnDel();
- assertFalse(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
- verify(mKeyListener, times(1)).onKeyDown((View) eq(null), same(mEditable),
- eq(event.getKeyCode()), same(event));
- }
-
- @Test
- public void testOnKeyDown_doesNotDelete_withAltModifier() {
- Selection.setSelection(mEditable, 0);
- final KeyEvent event = KeyboardUtil.altDel();
- assertFalse(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
- verify(mKeyListener, times(1)).onKeyDown((View) eq(null), same(mEditable),
- eq(event.getKeyCode()), same(event));
- }
-
- @Test
- public void testOnKeyDown_doesNotDelete_withCtrlModifier() {
- Selection.setSelection(mEditable, 0);
- final KeyEvent event = KeyboardUtil.ctrlDel();
- assertFalse(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
- verify(mKeyListener, times(1)).onKeyDown((View) eq(null), same(mEditable),
- eq(event.getKeyCode()), same(event));
- }
-
- @Test
- public void testOnKeyDown_doesNotDelete_withShiftModifier() {
- Selection.setSelection(mEditable, 0);
- final KeyEvent event = KeyboardUtil.shiftDel();
- assertFalse(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
- verify(mKeyListener, times(1)).onKeyDown((View) eq(null), same(mEditable),
- eq(event.getKeyCode()), same(event));
- }
-
- @Test
- public void testOnKeyDown_doesNotDelete_withSelection() {
- Selection.setSelection(mEditable, 0, mEditable.length());
- final KeyEvent event = KeyboardUtil.del();
- assertFalse(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
- verify(mKeyListener, times(1)).onKeyDown((View) eq(null), same(mEditable),
- eq(event.getKeyCode()), same(event));
- }
-
- @Test
- public void testOnKeyDown_doesNotDelete_withoutEmojiSpans() {
- Editable editable = new SpannableStringBuilder("abc");
- Selection.setSelection(editable, 1);
- final KeyEvent event = KeyboardUtil.del();
- assertFalse(mEmojiKeyListener.onKeyDown(null, editable, event.getKeyCode(), event));
- verify(mKeyListener, times(1)).onKeyDown((View) eq(null), same(editable),
- eq(event.getKeyCode()), same(event));
- }
-
- @Test
- public void testOnKeyDown_doesNotDelete_whenNoSpansBefore() {
- Selection.setSelection(mEditable, mTestString.emojiStartIndex());
- final KeyEvent event = KeyboardUtil.del();
- assertFalse(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
- verify(mKeyListener, times(1)).onKeyDown((View) eq(null), same(mEditable),
- eq(event.getKeyCode()), same(event));
- }
-
- @Test
- public void testOnKeyDown_deletesEmoji() {
- Selection.setSelection(mEditable, mTestString.emojiEndIndex());
- final KeyEvent event = KeyboardUtil.del();
- assertTrue(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
- verifyNoMoreInteractions(mKeyListener);
- }
-
- @Test
- public void testOnKeyDown_doesNotForwardDeleteEmoji_withNoSpansAfter() {
- Selection.setSelection(mEditable, mTestString.emojiEndIndex());
- final KeyEvent event = KeyboardUtil.forwardDel();
- assertFalse(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
- verify(mKeyListener, times(1)).onKeyDown((View) eq(null), same(mEditable),
- eq(event.getKeyCode()), same(event));
- }
-
- @Test
- public void testOnKeyDown_forwardDeletesEmoji() {
- Selection.setSelection(mEditable, mTestString.emojiStartIndex());
- final KeyEvent event = KeyboardUtil.forwardDel();
- assertTrue(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
- verifyNoMoreInteractions(mKeyListener);
- }
-}
diff --git a/emoji2/emoji2/src/androidTest/res/layout/activity_default.xml b/emoji2/emoji2/src/androidTest/res/layout/activity_default.xml
index 2d7968c..486dfbd 100644
--- a/emoji2/emoji2/src/androidTest/res/layout/activity_default.xml
+++ b/emoji2/emoji2/src/androidTest/res/layout/activity_default.xml
@@ -1,11 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/root"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical">
+ android:id="@+id/root"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
<TextView
android:id="@+id/text"
@@ -13,15 +12,8 @@
android:layout_height="wrap_content"
android:textSize="8sp"/>
- <androidx.emoji2.widget.EmojiEditText
+ <EditText
android:id="@+id/editText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
-
- <androidx.emoji2.widget.EmojiEditText
- android:id="@+id/editTextWithMaxCount"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- app:maxEmojiCount="5"/>
-
</LinearLayout>
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
index 1dc1ac4..86c3588 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
@@ -197,12 +197,12 @@
private static final Object INSTANCE_LOCK = new Object();
@GuardedBy("INSTANCE_LOCK")
- private static volatile EmojiCompat sInstance;
+ private static volatile @Nullable EmojiCompat sInstance;
- private final ReadWriteLock mInitLock;
+ private final @NonNull ReadWriteLock mInitLock;
@GuardedBy("mInitLock")
- private final Set<InitCallback> mInitCallbacks;
+ private final @NonNull Set<InitCallback> mInitCallbacks;
@GuardedBy("mInitLock")
@LoadState
@@ -211,18 +211,18 @@
/**
* Handler with main looper to run the callbacks on.
*/
- private final Handler mMainHandler;
+ private final @NonNull Handler mMainHandler;
/**
* Helper class for pre 19 compatibility.
*/
- private final CompatInternal mHelper;
+ private final @NonNull CompatInternal mHelper;
/**
* Metadata loader instance given in the Config instance.
*/
@SuppressWarnings("WeakerAccess") /* synthetic access */
- final MetadataRepoLoader mMetadataLoader;
+ final @NonNull MetadataRepoLoader mMetadataLoader;
/**
* @see Config#setReplaceAll(boolean)
@@ -240,7 +240,7 @@
* @see Config#setUseEmojiAsDefaultStyle(boolean, List)
*/
@SuppressWarnings("WeakerAccess") /* synthetic access */
- final int[] mEmojiAsDefaultStyleExceptions;
+ final @Nullable int[] mEmojiAsDefaultStyleExceptions;
/**
* @see Config#setEmojiSpanIndicatorEnabled(boolean)
@@ -299,15 +299,20 @@
* @see EmojiCompat.Config
*/
@SuppressWarnings("GuardedBy")
+ @NonNull
public static EmojiCompat init(@NonNull final Config config) {
- if (sInstance == null) {
+ EmojiCompat localInstance = sInstance;
+ if (localInstance == null) {
synchronized (INSTANCE_LOCK) {
- if (sInstance == null) {
- sInstance = new EmojiCompat(config);
+ // copy ref to local for nullness checker
+ localInstance = sInstance;
+ if (localInstance == null) {
+ localInstance = new EmojiCompat(config);
+ sInstance = localInstance;
}
}
}
- return sInstance;
+ return localInstance;
}
/**
@@ -319,11 +324,13 @@
@SuppressWarnings("GuardedBy")
@RestrictTo(LIBRARY_GROUP_PREFIX)
@VisibleForTesting
+ @NonNull
public static EmojiCompat reset(@NonNull final Config config) {
synchronized (INSTANCE_LOCK) {
- sInstance = new EmojiCompat(config);
+ EmojiCompat localInstance = new EmojiCompat(config);
+ sInstance = localInstance;
+ return localInstance;
}
- return sInstance;
}
/**
@@ -334,7 +341,8 @@
@SuppressWarnings("GuardedBy")
@RestrictTo(LIBRARY_GROUP_PREFIX)
@VisibleForTesting
- public static EmojiCompat reset(final EmojiCompat emojiCompat) {
+ @Nullable
+ public static EmojiCompat reset(@Nullable final EmojiCompat emojiCompat) {
synchronized (INSTANCE_LOCK) {
sInstance = emojiCompat;
}
@@ -349,11 +357,13 @@
*
* @throws IllegalStateException if called before {@link #init(EmojiCompat.Config)}
*/
+ @NonNull
public static EmojiCompat get() {
synchronized (INSTANCE_LOCK) {
- Preconditions.checkState(sInstance != null,
+ EmojiCompat localInstance = sInstance;
+ Preconditions.checkState(localInstance != null,
"EmojiCompat is not initialized. Please call EmojiCompat.init() first");
- return sInstance;
+ return localInstance;
}
}
@@ -538,7 +548,7 @@
* @return {@code true} if an {@link EmojiSpan} is deleted
*/
public static boolean handleOnKeyDown(@NonNull final Editable editable, final int keyCode,
- final KeyEvent event) {
+ @NonNull final KeyEvent event) {
if (Build.VERSION.SDK_INT >= 19) {
return EmojiProcessor.handleOnKeyDown(editable, keyCode, event);
} else {
@@ -621,11 +631,13 @@
* @throws IllegalStateException if not initialized yet
* @see #process(CharSequence, int, int)
*/
+ @Nullable
@CheckResult
- public CharSequence process(@NonNull final CharSequence charSequence) {
+ public CharSequence process(@Nullable final CharSequence charSequence) {
// since charSequence might be null here we have to check it. Passing through here to the
// main function so that it can do all the checks including isInitialized. It will also
// be the main point that decides what to return.
+
//noinspection ConstantConditions
@IntRange(from = 0) final int length = charSequence == null ? 0 : charSequence.length();
return process(charSequence, 0, length);
@@ -658,8 +670,9 @@
* {@code start > charSequence.length()},
* {@code end > charSequence.length()}
*/
+ @Nullable
@CheckResult
- public CharSequence process(@NonNull final CharSequence charSequence,
+ public CharSequence process(@Nullable final CharSequence charSequence,
@IntRange(from = 0) final int start, @IntRange(from = 0) final int end) {
return process(charSequence, start, end, EMOJI_COUNT_UNLIMITED);
}
@@ -694,8 +707,9 @@
* {@code end > charSequence.length()}
* {@code maxEmojiCount < 0}
*/
+ @Nullable
@CheckResult
- public CharSequence process(@NonNull final CharSequence charSequence,
+ public CharSequence process(@Nullable final CharSequence charSequence,
@IntRange(from = 0) final int start, @IntRange(from = 0) final int end,
@IntRange(from = 0) final int maxEmojiCount) {
return process(charSequence, start, end, maxEmojiCount, REPLACE_STRATEGY_DEFAULT);
@@ -735,8 +749,9 @@
* {@code end > charSequence.length()}
* {@code maxEmojiCount < 0}
*/
+ @Nullable
@CheckResult
- public CharSequence process(@NonNull final CharSequence charSequence,
+ public CharSequence process(@Nullable final CharSequence charSequence,
@IntRange(from = 0) final int start, @IntRange(from = 0) final int end,
@IntRange(from = 0) final int maxEmojiCount, @ReplaceStrategy int replaceStrategy) {
Preconditions.checkState(isInitialized(), "Not initialized yet");
@@ -944,14 +959,17 @@
*/
public abstract static class Config {
@SuppressWarnings("WeakerAccess") /* synthetic access */
+ @NonNull
final MetadataRepoLoader mMetadataLoader;
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mReplaceAll;
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mUseEmojiAsDefaultStyle;
@SuppressWarnings("WeakerAccess") /* synthetic access */
+ @Nullable
int[] mEmojiAsDefaultStyleExceptions;
@SuppressWarnings("WeakerAccess") /* synthetic access */
+ @Nullable
Set<InitCallback> mInitCallbacks;
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mEmojiSpanIndicatorEnabled;
@@ -960,6 +978,7 @@
@SuppressWarnings("WeakerAccess") /* synthetic access */
@LoadStrategy int mMetadataLoadStrategy = LOAD_STRATEGY_DEFAULT;
@SuppressWarnings("WeakerAccess") /* synthetic access */
+ @NonNull
GlyphChecker mGlyphChecker = new EmojiProcessor.DefaultGlyphChecker();
/**
@@ -979,6 +998,7 @@
*
* @return EmojiCompat.Config instance
*/
+ @NonNull
public Config registerInitCallback(@NonNull InitCallback initCallback) {
Preconditions.checkNotNull(initCallback, "initCallback cannot be null");
if (mInitCallbacks == null) {
@@ -997,6 +1017,7 @@
*
* @return EmojiCompat.Config instance
*/
+ @NonNull
public Config unregisterInitCallback(@NonNull InitCallback initCallback) {
Preconditions.checkNotNull(initCallback, "initCallback cannot be null");
if (mInitCallbacks != null) {
@@ -1014,6 +1035,7 @@
*
* @return EmojiCompat.Config instance
*/
+ @NonNull
public Config setReplaceAll(final boolean replaceAll) {
mReplaceAll = replaceAll;
return this;
@@ -1034,6 +1056,7 @@
* @param useEmojiAsDefaultStyle whether to use the emoji style presentation for all emojis
* that would be presented as text style by default
*/
+ @NonNull
public Config setUseEmojiAsDefaultStyle(final boolean useEmojiAsDefaultStyle) {
return setUseEmojiAsDefaultStyle(useEmojiAsDefaultStyle,
null);
@@ -1054,6 +1077,7 @@
* {@link #setUseEmojiAsDefaultStyle(boolean)} should
* be used instead.
*/
+ @NonNull
public Config setUseEmojiAsDefaultStyle(final boolean useEmojiAsDefaultStyle,
@Nullable final List<Integer> emojiAsDefaultStyleExceptions) {
mUseEmojiAsDefaultStyle = useEmojiAsDefaultStyle;
@@ -1078,6 +1102,7 @@
* @param emojiSpanIndicatorEnabled when {@code true} a background is drawn for each emoji
* that is replaced
*/
+ @NonNull
public Config setEmojiSpanIndicatorEnabled(boolean emojiSpanIndicatorEnabled) {
mEmojiSpanIndicatorEnabled = emojiSpanIndicatorEnabled;
return this;
@@ -1089,6 +1114,7 @@
*
* @see #setEmojiSpanIndicatorEnabled(boolean)
*/
+ @NonNull
public Config setEmojiSpanIndicatorColor(@ColorInt int color) {
mEmojiSpanIndicatorColor = color;
return this;
@@ -1130,6 +1156,7 @@
* {@link EmojiCompat#LOAD_STRATEGY_MANUAL}
*
*/
+ @NonNull
public Config setMetadataLoadStrategy(@LoadStrategy int strategy) {
mMetadataLoadStrategy = strategy;
return this;
@@ -1151,6 +1178,7 @@
/**
* Returns the {@link MetadataRepoLoader}.
*/
+ @NonNull
protected final MetadataRepoLoader getMetadataRepoLoader() {
return mMetadataLoader;
}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiDefaults.java
similarity index 62%
copy from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
copy to emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiDefaults.java
index 7e01354..9818af1 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiDefaults.java
@@ -1,5 +1,3 @@
-// ktlint-disable filename
-
/*
* Copyright 2021 The Android Open Source Project
*
@@ -16,6 +14,22 @@
* limitations under the License.
*/
-package androidx.compose.foundation
+package androidx.emoji2.text;
-internal actual typealias AtomicReference<V> = java.util.concurrent.atomic.AtomicReference<V>
\ No newline at end of file
+import androidx.annotation.RestrictTo;
+
+/**
+ * Defaults for emojicompat
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class EmojiDefaults {
+
+ private EmojiDefaults() {}
+
+ /**
+ * Default value for maxEmojiCount if not specified.
+ */
+ public static final int MAX_EMOJI_COUNT = Integer.MAX_VALUE;
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiMetadata.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiMetadata.java
index ba46f82..d6ef218 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiMetadata.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiMetadata.java
@@ -17,6 +17,7 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+import android.annotation.SuppressLint;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Typeface;
@@ -78,6 +79,7 @@
/**
* MetadataRepo that holds this instance.
*/
+ @NonNull
private final MetadataRepo mMetadataRepo;
/**
@@ -117,6 +119,7 @@
/**
* @return return typeface to be used to render this metadata
*/
+ @NonNull
public Typeface getTypeface() {
return mMetadataRepo.getTypeface();
}
@@ -181,6 +184,7 @@
* style selector 0xFE0F)
*/
@HasGlyph
+ @SuppressLint("KotlinPropertyAccess")
public int getHasGlyph() {
return mHasGlyph;
}
@@ -190,6 +194,7 @@
*
* @param hasGlyph {@code true} if system can render the emoji
*/
+ @SuppressLint("KotlinPropertyAccess")
public void setHasGlyph(boolean hasGlyph) {
mHasGlyph = hasGlyph ? HAS_GLYPH_EXISTS : HAS_GLYPH_ABSENT;
}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiProcessor.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiProcessor.java
index d8c4765..98849eb 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiProcessor.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiProcessor.java
@@ -37,7 +37,6 @@
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.graphics.PaintCompat;
-import androidx.emoji2.widget.SpannableBuilder;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -81,16 +80,19 @@
/**
* Factory used to create EmojiSpans.
*/
+ @NonNull
private final EmojiCompat.SpanFactory mSpanFactory;
/**
* Emoji metadata repository.
*/
+ @NonNull
private final MetadataRepo mMetadataRepo;
/**
* Utility class that checks if the system can render a given glyph.
*/
+ @NonNull
private EmojiCompat.GlyphChecker mGlyphChecker;
/**
@@ -101,6 +103,7 @@
/**
* @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean, List)
*/
+ @Nullable
private final int[] mEmojiAsDefaultStyleExceptions;
EmojiProcessor(
@@ -298,7 +301,7 @@
* @return {@code true} if an {@link EmojiSpan} is deleted
*/
static boolean handleOnKeyDown(@NonNull final Editable editable, final int keyCode,
- final KeyEvent event) {
+ @NonNull final KeyEvent event) {
final boolean handled;
switch (keyCode) {
case KeyEvent.KEYCODE_DEL:
@@ -320,7 +323,7 @@
return false;
}
- private static boolean delete(final Editable content, final KeyEvent event,
+ private static boolean delete(@NonNull final Editable content, @NonNull final KeyEvent event,
final boolean forwardDelete) {
if (hasModifiers(event)) {
return false;
@@ -431,7 +434,7 @@
return start == -1 || end == -1 || start != end;
}
- private static boolean hasModifiers(KeyEvent event) {
+ private static boolean hasModifiers(@NonNull KeyEvent event) {
return !KeyEvent.metaStateHasNoModifiers(event.getMetaState());
}
@@ -651,9 +654,12 @@
/**
* Copy of BaseInputConnection findIndexBackward and findIndexForward functions.
*/
+ @RequiresApi(19)
private static final class CodepointIndexFinder {
private static final int INVALID_INDEX = -1;
+ private CodepointIndexFinder() {}
+
/**
* Find start index of the character in {@code cs} that is {@code numCodePoints} behind
* starting from {@code from}.
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiSpan.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiSpan.java
index 70dd537..dd07ff1 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiSpan.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiSpan.java
@@ -17,10 +17,12 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+import android.annotation.SuppressLint;
import android.graphics.Paint;
import android.text.style.ReplacementSpan;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
@@ -43,6 +45,7 @@
* representing same emoji to be in memory. When unparcelled, EmojiSpan tries to set it back
* using the singleton EmojiCompat instance.
*/
+ @NonNull
private final EmojiMetadata mMetadata;
/**
@@ -74,8 +77,11 @@
}
@Override
- public int getSize(@NonNull final Paint paint, final CharSequence text, final int start,
- final int end, final Paint.FontMetricsInt fm) {
+ public int getSize(@NonNull final Paint paint,
+ @SuppressLint("UnknownNullness") final CharSequence text,
+ final int start,
+ final int end,
+ @Nullable final Paint.FontMetricsInt fm) {
paint.getFontMetricsInt(mTmpFontMetrics);
final int fontHeight = Math.abs(mTmpFontMetrics.descent - mTmpFontMetrics.ascent);
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java
index 4ade79f..f20049c 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java
@@ -140,7 +140,8 @@
* of {@code null}, the metadata loader creates own {@link HandlerThread} for
* initialization.
*/
- public FontRequestEmojiCompatConfig setHandler(Handler handler) {
+ @NonNull
+ public FontRequestEmojiCompatConfig setHandler(@Nullable Handler handler) {
((FontRequestMetadataLoader) getMetadataRepoLoader()).setHandler(handler);
return this;
}
@@ -153,7 +154,8 @@
* file. Can be {@code null}. In case of {@code null}, the metadata loader never
* retries.
*/
- public FontRequestEmojiCompatConfig setRetryPolicy(RetryPolicy policy) {
+ @NonNull
+ public FontRequestEmojiCompatConfig setRetryPolicy(@Nullable RetryPolicy policy) {
((FontRequestMetadataLoader) getMetadataRepoLoader()).setRetryPolicy(policy);
return this;
}
@@ -163,23 +165,23 @@
* given FontRequest.
*/
private static class FontRequestMetadataLoader implements EmojiCompat.MetadataRepoLoader {
- private final Context mContext;
- private final FontRequest mRequest;
- private final FontProviderHelper mFontProviderHelper;
+ private final @NonNull Context mContext;
+ private final @NonNull FontRequest mRequest;
+ private final @NonNull FontProviderHelper mFontProviderHelper;
- private final Object mLock = new Object();
+ private final @NonNull Object mLock = new Object();
@GuardedBy("mLock")
- private Handler mHandler;
+ private @Nullable Handler mHandler;
@GuardedBy("mLock")
- private HandlerThread mThread;
+ private @Nullable HandlerThread mThread;
@GuardedBy("mLock")
private @Nullable RetryPolicy mRetryPolicy;
// Following three variables must be touched only on the thread associated with mHandler.
@SuppressWarnings("WeakerAccess") /* synthetic access */
EmojiCompat.MetadataRepoLoaderCallback mCallback;
- private ContentObserver mObserver;
- private Runnable mHandleMetadataCreationRunner;
+ private @Nullable ContentObserver mObserver;
+ private @Nullable Runnable mHandleMetadataCreationRunner;
FontRequestMetadataLoader(@NonNull Context context, @NonNull FontRequest request,
@NonNull FontProviderHelper fontProviderHelper) {
@@ -190,13 +192,13 @@
mFontProviderHelper = fontProviderHelper;
}
- public void setHandler(Handler handler) {
+ public void setHandler(@Nullable Handler handler) {
synchronized (mLock) {
mHandler = handler;
}
}
- public void setRetryPolicy(RetryPolicy policy) {
+ public void setRetryPolicy(@Nullable RetryPolicy policy) {
synchronized (mLock) {
mRetryPolicy = policy;
}
@@ -332,12 +334,14 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public static class FontProviderHelper {
/** Calls FontsContractCompat.fetchFonts. */
+ @NonNull
public FontFamilyResult fetchFonts(@NonNull Context context,
@NonNull FontRequest request) throws NameNotFoundException {
return FontsContractCompat.fetchFonts(context, null /* cancellation signal */, request);
}
/** Calls FontsContractCompat.buildTypeface. */
+ @Nullable
public Typeface buildTypeface(@NonNull Context context,
@NonNull FontsContractCompat.FontInfo font) throws NameNotFoundException {
return FontsContractCompat.buildTypeface(context, null /* cancellation signal */,
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataListReader.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataListReader.java
index f9f397f..6db7379 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataListReader.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataListReader.java
@@ -21,6 +21,7 @@
import androidx.annotation.AnyThread;
import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.text.emoji.flatbuffer.MetadataList;
@@ -236,9 +237,9 @@
*/
private static class InputStreamOpenTypeReader implements OpenTypeReader {
- private final byte[] mByteArray;
- private final ByteBuffer mByteBuffer;
- private final InputStream mInputStream;
+ private final @NonNull byte[] mByteArray;
+ private final @NonNull ByteBuffer mByteBuffer;
+ private final @NonNull InputStream mInputStream;
private long mPosition = 0;
/**
@@ -247,7 +248,7 @@
*
* @param inputStream InputStream to read from
*/
- InputStreamOpenTypeReader(final InputStream inputStream) {
+ InputStreamOpenTypeReader(@NonNull final InputStream inputStream) {
mInputStream = inputStream;
mByteArray = new byte[UINT32_BYTE_COUNT];
mByteBuffer = ByteBuffer.wrap(mByteArray);
@@ -306,14 +307,14 @@
*/
private static class ByteBufferReader implements OpenTypeReader {
- private final ByteBuffer mByteBuffer;
+ private final @NonNull ByteBuffer mByteBuffer;
/**
* Constructs the reader with the given ByteBuffer.
*
* @param byteBuffer ByteBuffer to read from
*/
- ByteBufferReader(final ByteBuffer byteBuffer) {
+ ByteBufferReader(@NonNull final ByteBuffer byteBuffer) {
mByteBuffer = byteBuffer;
mByteBuffer.order(ByteOrder.BIG_ENDIAN);
}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java
index 2f5ea66..00fef53 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java
@@ -47,36 +47,23 @@
/**
* MetadataList that contains the emoji metadata.
*/
- private final MetadataList mMetadataList;
+ private final @NonNull MetadataList mMetadataList;
/**
* char presentation of all EmojiMetadata's in a single array. All emojis we have are mapped to
* Private Use Area A, in the range U+F0000..U+FFFFD. Therefore each emoji takes 2 chars.
*/
- private final char[] mEmojiCharArray;
+ private final @NonNull char[] mEmojiCharArray;
/**
* Empty root node of the trie.
*/
- private final Node mRootNode;
+ private final @NonNull Node mRootNode;
/**
* Typeface to be used to render emojis.
*/
- private final Typeface mTypeface;
-
- /**
- * Constructor used for tests.
- *
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP_PREFIX)
- MetadataRepo() {
- mTypeface = null;
- mMetadataList = null;
- mRootNode = new Node(DEFAULT_ROOT_SIZE);
- mEmojiCharArray = new char[0];
- }
+ private final @NonNull Typeface mTypeface;
/**
* Private constructor that is called by one of {@code create} methods.
@@ -94,12 +81,22 @@
}
/**
+ * Construct MetadataRepo from a preloaded MetadatList.
+ */
+ @NonNull
+ static MetadataRepo create(@NonNull final Typeface typeface,
+ @NonNull final MetadataList metadataList) {
+ return new MetadataRepo(typeface, metadataList);
+ }
+
+ /**
* Construct MetadataRepo from an input stream. The library does not close the given
* InputStream, therefore it is caller's responsibility to properly close the stream.
*
* @param typeface Typeface to be used to render emojis
* @param inputStream InputStream to read emoji metadata from
*/
+ @NonNull
public static MetadataRepo create(@NonNull final Typeface typeface,
@NonNull final InputStream inputStream) throws IOException {
return new MetadataRepo(typeface, MetadataListReader.read(inputStream));
@@ -112,6 +109,7 @@
* @param typeface Typeface to be used to render emojis
* @param byteBuffer ByteBuffer to read emoji metadata from
*/
+ @NonNull
public static MetadataRepo create(@NonNull final Typeface typeface,
@NonNull final ByteBuffer byteBuffer) throws IOException {
return new MetadataRepo(typeface, MetadataListReader.read(byteBuffer));
@@ -124,8 +122,9 @@
* @param assetPath asset manager path of the file that the Typeface and metadata will be
* created from
*/
+ @NonNull
public static MetadataRepo create(@NonNull final AssetManager assetManager,
- final String assetPath) throws IOException {
+ @NonNull final String assetPath) throws IOException {
final Typeface typeface = Typeface.createFromAsset(assetManager, assetPath);
return new MetadataRepo(typeface, MetadataListReader.read(assetManager, assetPath));
}
@@ -148,6 +147,7 @@
/**
* @hide
*/
+ @NonNull
@RestrictTo(LIBRARY_GROUP_PREFIX)
Typeface getTypeface() {
return mTypeface;
@@ -164,6 +164,7 @@
/**
* @hide
*/
+ @NonNull
@RestrictTo(LIBRARY_GROUP_PREFIX)
Node getRootNode() {
return mRootNode;
@@ -172,6 +173,7 @@
/**
* @hide
*/
+ @NonNull
@RestrictTo(LIBRARY_GROUP_PREFIX)
public char[] getEmojiCharArray() {
return mEmojiCharArray;
@@ -180,6 +182,7 @@
/**
* @hide
*/
+ @NonNull
@RestrictTo(LIBRARY_GROUP_PREFIX)
public MetadataList getMetadataList() {
return mMetadataList;
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/SpannableBuilder.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/SpannableBuilder.java
similarity index 89%
rename from emoji2/emoji2/src/main/java/androidx/emoji2/widget/SpannableBuilder.java
rename to emoji2/emoji2/src/main/java/androidx/emoji2/text/SpannableBuilder.java
index 97fd4e7..01399a4 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/SpannableBuilder.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/SpannableBuilder.java
@@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.widget;
+package androidx.emoji2.text;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+import android.annotation.SuppressLint;
import android.text.Editable;
import android.text.SpanWatcher;
import android.text.Spannable;
@@ -27,7 +28,6 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.util.Preconditions;
-import androidx.emoji2.text.EmojiSpan;
import java.lang.reflect.Array;
import java.util.ArrayList;
@@ -46,19 +46,18 @@
* the framework.
*
* @hide
- * @see EmojiEditableFactory
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public final class SpannableBuilder extends SpannableStringBuilder {
/**
* DynamicLayout$ChangeWatcher class.
*/
- private final Class<?> mWatcherClass;
+ private final @NonNull Class<?> mWatcherClass;
/**
* All WatcherWrappers.
*/
- private final List<WatcherWrapper> mWatchers = new ArrayList<>();
+ private final @NonNull List<WatcherWrapper> mWatchers = new ArrayList<>();
/**
* @hide
@@ -93,8 +92,9 @@
/**
* @hide
*/
+ @NonNull
@RestrictTo(LIBRARY_GROUP_PREFIX)
- static SpannableBuilder create(@NonNull Class<?> clazz, @NonNull CharSequence text) {
+ public static SpannableBuilder create(@NonNull Class<?> clazz, @NonNull CharSequence text) {
return new SpannableBuilder(clazz, text);
}
@@ -120,6 +120,7 @@
return mWatcherClass == clazz;
}
+ @SuppressLint("UnknownNullness")
@Override
public CharSequence subSequence(int start, int end) {
return new SpannableBuilder(mWatcherClass, this, start, end);
@@ -131,7 +132,7 @@
* this new mObject as the span.
*/
@Override
- public void setSpan(Object what, int start, int end, int flags) {
+ public void setSpan(@Nullable Object what, int start, int end, int flags) {
if (isWatcher(what)) {
final WatcherWrapper span = new WatcherWrapper(what);
mWatchers.add(span);
@@ -144,9 +145,10 @@
* If previously a DynamicLayout$ChangeWatcher was wrapped in a WatcherWrapper, return the
* correct Object that the client has set.
*/
+ @SuppressLint("UnknownNullness")
@SuppressWarnings("unchecked")
@Override
- public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) {
+ public <T> T[] getSpans(int queryStart, int queryEnd, @NonNull Class<T> kind) {
if (isWatcher(kind)) {
final WatcherWrapper[] spans = super.getSpans(queryStart, queryEnd,
WatcherWrapper.class);
@@ -164,7 +166,7 @@
* instead.
*/
@Override
- public void removeSpan(Object what) {
+ public void removeSpan(@Nullable Object what) {
final WatcherWrapper watcher;
if (isWatcher(what)) {
watcher = getWatcherFor(what);
@@ -186,7 +188,7 @@
* Return the correct start for the DynamicLayout$ChangeWatcher span.
*/
@Override
- public int getSpanStart(Object tag) {
+ public int getSpanStart(@Nullable Object tag) {
if (isWatcher(tag)) {
final WatcherWrapper watcher = getWatcherFor(tag);
if (watcher != null) {
@@ -200,7 +202,7 @@
* Return the correct end for the DynamicLayout$ChangeWatcher span.
*/
@Override
- public int getSpanEnd(Object tag) {
+ public int getSpanEnd(@Nullable Object tag) {
if (isWatcher(tag)) {
final WatcherWrapper watcher = getWatcherFor(tag);
if (watcher != null) {
@@ -214,7 +216,7 @@
* Return the correct flags for the DynamicLayout$ChangeWatcher span.
*/
@Override
- public int getSpanFlags(Object tag) {
+ public int getSpanFlags(@Nullable Object tag) {
if (isWatcher(tag)) {
final WatcherWrapper watcher = getWatcherFor(tag);
if (watcher != null) {
@@ -228,8 +230,8 @@
* Return the correct transition for the DynamicLayout$ChangeWatcher span.
*/
@Override
- public int nextSpanTransition(int start, int limit, Class type) {
- if (isWatcher(type)) {
+ public int nextSpanTransition(int start, int limit, @Nullable Class type) {
+ if (type == null || isWatcher(type)) {
type = WatcherWrapper.class;
}
return super.nextSpanTransition(start, limit, type);
@@ -297,6 +299,7 @@
}
}
+ @SuppressLint("UnknownNullness")
@Override
public SpannableStringBuilder replace(int start, int end, CharSequence tb) {
blockWatchers();
@@ -305,6 +308,7 @@
return this;
}
+ @SuppressLint("UnknownNullness")
@Override
public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart,
int tbend) {
@@ -314,42 +318,51 @@
return this;
}
+ @SuppressLint("UnknownNullness")
@Override
public SpannableStringBuilder insert(int where, CharSequence tb) {
super.insert(where, tb);
return this;
}
+ @SuppressLint("UnknownNullness")
@Override
public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) {
super.insert(where, tb, start, end);
return this;
}
+ @SuppressLint("UnknownNullness")
@Override
public SpannableStringBuilder delete(int start, int end) {
super.delete(start, end);
return this;
}
+ @NonNull
@Override
- public SpannableStringBuilder append(CharSequence text) {
+ public SpannableStringBuilder append(@SuppressLint("UnknownNullness") CharSequence text) {
super.append(text);
return this;
}
+ @NonNull
@Override
public SpannableStringBuilder append(char text) {
super.append(text);
return this;
}
+ @NonNull
@Override
- public SpannableStringBuilder append(CharSequence text, int start, int end) {
+ public SpannableStringBuilder append(@SuppressLint("UnknownNullness") CharSequence text,
+ int start,
+ int end) {
super.append(text, start, end);
return this;
}
+ @SuppressLint("UnknownNullness")
@Override
public SpannableStringBuilder append(CharSequence text, Object what, int flags) {
super.append(text, what, flags);
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/TypefaceEmojiSpan.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/TypefaceEmojiSpan.java
index 2c3ab58..5eb3ff9 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/TypefaceEmojiSpan.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/TypefaceEmojiSpan.java
@@ -17,12 +17,14 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+import android.annotation.SuppressLint;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.TextPaint;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
@@ -38,19 +40,20 @@
/**
* Paint object used to draw a background in debug mode.
*/
- private static Paint sDebugPaint;
+ private static @Nullable Paint sDebugPaint;
/**
* Default constructor.
*
* @param metadata metadata representing the emoji that this span will draw
*/
- public TypefaceEmojiSpan(final EmojiMetadata metadata) {
+ public TypefaceEmojiSpan(final @NonNull EmojiMetadata metadata) {
super(metadata);
}
@Override
- public void draw(@NonNull final Canvas canvas, final CharSequence text,
+ public void draw(@NonNull final Canvas canvas,
+ @SuppressLint("UnknownNullness") final CharSequence text,
@IntRange(from = 0) final int start, @IntRange(from = 0) final int end, final float x,
final int top, final int y, final int bottom, @NonNull final Paint paint) {
if (EmojiCompat.get().isEmojiSpanIndicatorEnabled()) {
@@ -59,6 +62,7 @@
getMetadata().draw(canvas, x, y, paint);
}
+ @NonNull
private static Paint getDebugPaint() {
if (sDebugPaint == null) {
sDebugPaint = new TextPaint();
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiInputConnection.java b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiInputConnection.java
deleted file mode 100644
index 53e8ee4..0000000
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiInputConnection.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.emoji2.widget;
-
-import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
-
-import android.text.Editable;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
-import android.view.inputmethod.InputConnectionWrapper;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-import androidx.emoji2.text.EmojiCompat;
-
-/**
- * InputConnectionWrapper for EditText delete operations. Keyboard does not have knowledge about
- * emojis and therefore might send commands to delete a part of the emoji sequence which creates
- * invalid codeunits/getCodepointAt in the text.
- * <p/>
- * This class tries to correctly delete an emoji checking if there is an emoji span.
- *
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP_PREFIX)
-@RequiresApi(19)
-final class EmojiInputConnection extends InputConnectionWrapper {
- private final TextView mTextView;
-
- EmojiInputConnection(
- @NonNull final TextView textView,
- @NonNull final InputConnection inputConnection,
- @NonNull final EditorInfo outAttrs) {
- super(inputConnection, false);
- mTextView = textView;
- EmojiCompat.get().updateEditorInfoAttrs(outAttrs);
- }
-
- @Override
- public boolean deleteSurroundingText(final int beforeLength, final int afterLength) {
- final boolean result = EmojiCompat.handleDeleteSurroundingText(this, getEditable(),
- beforeLength, afterLength, false /*inCodePoints*/);
- return result || super.deleteSurroundingText(beforeLength, afterLength);
- }
-
- @Override
- public boolean deleteSurroundingTextInCodePoints(final int beforeLength,
- final int afterLength) {
- final boolean result = EmojiCompat.handleDeleteSurroundingText(this, getEditable(),
- beforeLength, afterLength, true /*inCodePoints*/);
- return result || super.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
- }
-
- private Editable getEditable() {
- return mTextView.getEditableText();
- }
-}
diff --git a/fragment/fragment/api/current.txt b/fragment/fragment/api/current.txt
index a700d16..b850c56 100644
--- a/fragment/fragment/api/current.txt
+++ b/fragment/fragment/api/current.txt
@@ -427,6 +427,8 @@
field public static final int TRANSIT_EXIT_MASK = 8192; // 0x2000
field public static final int TRANSIT_FRAGMENT_CLOSE = 8194; // 0x2002
field public static final int TRANSIT_FRAGMENT_FADE = 4099; // 0x1003
+ field public static final int TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE = 8197; // 0x2005
+ field public static final int TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN = 4100; // 0x1004
field public static final int TRANSIT_FRAGMENT_OPEN = 4097; // 0x1001
field public static final int TRANSIT_NONE = 0; // 0x0
field public static final int TRANSIT_UNSET = -1; // 0xffffffff
diff --git a/fragment/fragment/api/public_plus_experimental_current.txt b/fragment/fragment/api/public_plus_experimental_current.txt
index a2f6d9f..97a49c7 100644
--- a/fragment/fragment/api/public_plus_experimental_current.txt
+++ b/fragment/fragment/api/public_plus_experimental_current.txt
@@ -433,6 +433,8 @@
field public static final int TRANSIT_EXIT_MASK = 8192; // 0x2000
field public static final int TRANSIT_FRAGMENT_CLOSE = 8194; // 0x2002
field public static final int TRANSIT_FRAGMENT_FADE = 4099; // 0x1003
+ field public static final int TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE = 8197; // 0x2005
+ field public static final int TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN = 4100; // 0x1004
field public static final int TRANSIT_FRAGMENT_OPEN = 4097; // 0x1001
field public static final int TRANSIT_NONE = 0; // 0x0
field public static final int TRANSIT_UNSET = -1; // 0xffffffff
diff --git a/fragment/fragment/api/restricted_current.txt b/fragment/fragment/api/restricted_current.txt
index 060b006..ab90399 100644
--- a/fragment/fragment/api/restricted_current.txt
+++ b/fragment/fragment/api/restricted_current.txt
@@ -435,6 +435,8 @@
field public static final int TRANSIT_EXIT_MASK = 8192; // 0x2000
field public static final int TRANSIT_FRAGMENT_CLOSE = 8194; // 0x2002
field public static final int TRANSIT_FRAGMENT_FADE = 4099; // 0x1003
+ field public static final int TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE = 8197; // 0x2005
+ field public static final int TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN = 4100; // 0x1004
field public static final int TRANSIT_FRAGMENT_OPEN = 4097; // 0x1001
field public static final int TRANSIT_NONE = 0; // 0x0
field public static final int TRANSIT_UNSET = -1; // 0xffffffff
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSharedElementTransitionTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSharedElementTransitionTest.kt
new file mode 100644
index 0000000..820b9762
--- /dev/null
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSharedElementTransitionTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.fragment.app
+
+import android.os.Build
+import androidx.core.view.ViewCompat
+import androidx.fragment.app.test.FragmentTestActivity
+import androidx.fragment.test.R
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.testutils.withActivity
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+class FragmentSharedElementTransitionTest {
+
+ @Test
+ fun testNestedSharedElementView() {
+ with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+ val fragment = TransitionFragment(R.layout.nested_transition_groups)
+ withActivity {
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.content, fragment)
+ .commit()
+ }
+
+ val squareContainer = withActivity { findViewById(R.id.squareContainer) }
+ var blueSquare = withActivity { findViewById(R.id.blueSquare) }
+
+ withActivity {
+ supportFragmentManager
+ .beginTransaction()
+ .addSharedElement(squareContainer, "squareContainer")
+ .addSharedElement(blueSquare, "blueSquare")
+ .replace(R.id.content, TransitionFragment(R.layout.nested_transition_groups))
+ .commit()
+ }
+
+ blueSquare = withActivity { findViewById(R.id.blueSquare) }
+
+ assertThat(ViewCompat.getTransitionName(blueSquare)).isEqualTo("blueSquare")
+ }
+ }
+}
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitTest.kt
index 1ded023..0f9a36a 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitTest.kt
@@ -18,6 +18,9 @@
import android.animation.Animator
import android.view.animation.Animation
+import androidx.annotation.LayoutRes
+import androidx.fragment.app.FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE
+import androidx.fragment.app.FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN
import androidx.fragment.app.test.FragmentTestActivity
import androidx.fragment.test.R
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -35,7 +38,6 @@
@LargeTest
@RunWith(AndroidJUnit4::class)
class FragmentTransitTest {
-
@Suppress("DEPRECATION")
@get:Rule
var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
@@ -62,11 +64,57 @@
.containsExactly(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
}
- class TransitFragment : StrictViewFragment() {
+ @Test
+ fun testFragmentAnimationWithActivityTransition() {
+ val fragmentA = FragmentA()
+ val fragmentB = FragmentB()
+ val fm = activityRule.activity.supportFragmentManager
+
+ // set TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN to navigate forward when fragmentA entering.
+ fm.beginTransaction()
+ .add(R.id.fragmentContainer, fragmentA)
+ .setTransition(TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN)
+ .commit()
+ activityRule.executePendingTransactions(fm)
+
+ assertThat(fragmentA.transitValues).containsExactly(TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN)
+ assertThat(fragmentA.isEnterTransit).isTrue()
+
+ // set TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN to navigate forward when fragmentB entering
+ // and fragmentA exiting.
+ fm.beginTransaction()
+ .replace(R.id.fragmentContainer, fragmentB)
+ .setTransition(TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN)
+ .addToBackStack(null)
+ .commit()
+ activityRule.executePendingTransactions(fm)
+
+ assertThat(fragmentA.transitValues).containsExactly(TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN)
+ assertThat(fragmentA.isEnterTransit).isFalse()
+ assertThat(fragmentB.transitValues).containsExactly(TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN)
+ assertThat(fragmentB.isEnterTransit).isTrue()
+
+ // Simulating back key with popBackStack, system will set
+ // TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE to navigate backward when fragmentB exiting
+ // and fragmentA entering.
+ fm.popBackStack()
+ activityRule.executePendingTransactions(fm)
+
+ assertThat(fragmentB.transitValues).contains(TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE)
+ assertThat(fragmentB.isEnterTransit).isFalse()
+ assertThat(fragmentA.transitValues).contains(TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE)
+ assertThat(fragmentA.isEnterTransit).isTrue()
+ }
+
+ public open class TransitFragment(
+ @LayoutRes contentLayoutId: Int = R.layout.strict_view_fragment
+ ) : StrictFragment(contentLayoutId) {
val transitValues = mutableSetOf<Int>()
+ var isEnterTransit: Boolean = false
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
transitValues += transit
+ isEnterTransit = enter
return super.onCreateAnimation(transit, enter, nextAnim)
}
@@ -75,4 +123,8 @@
return super.onCreateAnimator(transit, enter, nextAnim)
}
}
+
+ class FragmentA : TransitFragment(R.layout.fragment_a)
+
+ class FragmentB : TransitFragment(R.layout.fragment_b)
}
diff --git a/fragment/fragment/src/androidTest/res/layout/nested_transition_groups.xml b/fragment/fragment/src/androidTest/res/layout/nested_transition_groups.xml
new file mode 100644
index 0000000..f6b90e9
--- /dev/null
+++ b/fragment/fragment/src/androidTest/res/layout/nested_transition_groups.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/squareContainer"
+ android:transitionName="squareContainer"
+ android:transitionGroup="false"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <View android:id="@+id/blueSquare"
+ android:transitionName="blueSquare"
+ android:layout_width="100dp"
+ android:layout_height="100dp"
+ android:background="#008"/>
+</FrameLayout>
\ No newline at end of file
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.java b/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.java
index aa72c76..247065a 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/DefaultSpecialEffectsController.java
@@ -708,7 +708,9 @@
}
}
} else {
- transitioningViews.add(view);
+ if (!transitioningViews.contains(view)) {
+ transitioningViews.add(view);
+ }
}
}
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentAnim.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentAnim.java
index 76f34ef..df536f1 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentAnim.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentAnim.java
@@ -21,6 +21,7 @@
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.content.res.Resources;
+import android.content.res.TypedArray;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
@@ -60,6 +61,7 @@
if (fragment.mContainer != null && fragment.mContainer.getLayoutTransition() != null) {
return null;
}
+
Animation animation = fragment.onCreateAnimation(transit, enter, nextAnim);
if (animation != null) {
return new AnimationOrAnimator(animation);
@@ -71,10 +73,9 @@
}
if (nextAnim == 0 && transit != 0) {
- nextAnim = transitToAnimResourceId(transit, enter);
+ nextAnim = transitToAnimResourceId(context, transit, enter);
}
-
if (nextAnim != 0) {
String dir = context.getResources().getResourceTypeName(nextAnim);
boolean isAnim = "anim".equals(dir);
@@ -195,7 +196,8 @@
}
@AnimRes
- private static int transitToAnimResourceId(int transit, boolean enter) {
+ private static int transitToAnimResourceId(@NonNull Context context, int transit,
+ boolean enter) {
int animAttr = -1;
switch (transit) {
case FragmentTransaction.TRANSIT_FRAGMENT_OPEN:
@@ -207,10 +209,32 @@
case FragmentTransaction.TRANSIT_FRAGMENT_FADE:
animAttr = enter ? R.animator.fragment_fade_enter : R.animator.fragment_fade_exit;
break;
+ case FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN:
+ animAttr = enter
+ ? toActivityTransitResId(context, android.R.attr.activityOpenEnterAnimation)
+ : toActivityTransitResId(context, android.R.attr.activityOpenExitAnimation);
+ break;
+ case FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE:
+ animAttr = enter
+ ? toActivityTransitResId(context,
+ android.R.attr.activityCloseEnterAnimation)
+ : toActivityTransitResId(context,
+ android.R.attr.activityCloseExitAnimation);
+ break;
}
return animAttr;
}
+ @AnimRes
+ private static int toActivityTransitResId(@NonNull Context context, int attrInt) {
+ int resId;
+ TypedArray typedArray = context.obtainStyledAttributes(
+ android.R.style.Animation_Activity, new int[]{attrInt});
+ resId = typedArray.getResourceId(0, View.NO_ID);
+ typedArray.recycle();
+ return resId;
+ }
+
/**
* Contains either an animator or animation. One of these should be null.
*/
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 811156c..6abf46e 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -3618,6 +3618,12 @@
case FragmentTransaction.TRANSIT_FRAGMENT_FADE:
rev = FragmentTransaction.TRANSIT_FRAGMENT_FADE;
break;
+ case FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN:
+ rev = FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE;
+ break;
+ case FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE:
+ rev = FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN;
+ break;
}
return rev;
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransaction.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransaction.java
index 0140639..b94e94c 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransaction.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransaction.java
@@ -504,7 +504,8 @@
/** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
- @IntDef({TRANSIT_NONE, TRANSIT_FRAGMENT_OPEN, TRANSIT_FRAGMENT_CLOSE, TRANSIT_FRAGMENT_FADE})
+ @IntDef({TRANSIT_NONE, TRANSIT_FRAGMENT_OPEN, TRANSIT_FRAGMENT_CLOSE, TRANSIT_FRAGMENT_FADE,
+ TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN, TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE})
@Retention(RetentionPolicy.SOURCE)
private @interface Transit {}
@@ -521,6 +522,22 @@
public static final int TRANSIT_FRAGMENT_FADE = 3 | TRANSIT_ENTER_MASK;
/**
+ * Fragment is being added onto the stack with Activity open transition.
+ *
+ * @see android.R.attr#activityOpenEnterAnimation
+ * @see android.R.attr#activityOpenExitAnimation
+ */
+ public static final int TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN = 4 | TRANSIT_ENTER_MASK;
+
+ /**
+ * Fragment is being removed from the stack with Activity close transition.
+ *
+ * @see android.R.attr#activityCloseEnterAnimation
+ * @see android.R.attr#activityCloseExitAnimation
+ */
+ public static final int TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE = 5 | TRANSIT_EXIT_MASK;
+
+ /**
* Set specific animation resources to run for the fragments that are
* entering and exiting in this transaction. These animations will not be
* played when popping the back stack.
diff --git a/hilt/hilt-navigation/lint-baseline.xml b/hilt/hilt-navigation/lint-baseline.xml
deleted file mode 100644
index 8f1aa4b..0000000
--- a/hilt/hilt-navigation/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/inspection/inspection-testing/lint-baseline.xml b/inspection/inspection-testing/lint-baseline.xml
deleted file mode 100644
index 8f1aa4b..0000000
--- a/inspection/inspection-testing/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/inspection/inspection/lint-baseline.xml b/inspection/inspection/lint-baseline.xml
index 05edf40..367e8df 100644
--- a/inspection/inspection/lint-baseline.xml
+++ b/inspection/inspection/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
+<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
<issue
id="UnknownNullness"
diff --git a/jetifier/jetifier/migration.config b/jetifier/jetifier/migration.config
index bd39e34..de39296 100644
--- a/jetifier/jetifier/migration.config
+++ b/jetifier/jetifier/migration.config
@@ -485,11 +485,7 @@
"to": "android/support/v4/app/SpecialEffectsController{0}"
},
{
- "from": "androidx/fragment/app/strictmode/FragmentStrictMode(.*)",
- "to": "ignore"
- },
- {
- "from": "androidx/fragment/app/strictmode/Violation(.*)",
+ "from": "androidx/fragment/app/strictmode/(.*)",
"to": "ignore"
},
{
diff --git a/jetifier/jetifier/standalone/src/main/kotlin/com/android/tools/build/jetifier/standalone/TopOfTreeBuilder.kt b/jetifier/jetifier/standalone/src/main/kotlin/com/android/tools/build/jetifier/standalone/TopOfTreeBuilder.kt
index d843e8b..b431873 100644
--- a/jetifier/jetifier/standalone/src/main/kotlin/com/android/tools/build/jetifier/standalone/TopOfTreeBuilder.kt
+++ b/jetifier/jetifier/standalone/src/main/kotlin/com/android/tools/build/jetifier/standalone/TopOfTreeBuilder.kt
@@ -57,14 +57,16 @@
val name = pomFile.relativePath.toFile().nameWithoutExtension
val nameAar = name + ".aar"
val nameJar = name + ".jar"
- val artifactFile = libFiles.first {
+ val artifactFile = libFiles.firstOrNull {
it.fileName == nameAar || it.fileName == nameJar
}
val nameSources = name + "-sources.jar"
- val sourcesFile = libFiles.first {
+ val sourcesFile = libFiles.firstOrNull {
it.fileName == nameSources
}
- process(pomFile, artifactFile, sourcesFile, newFiles)
+ if (artifactFile != null && sourcesFile != null) {
+ process(pomFile, artifactFile, sourcesFile, newFiles)
+ }
}
}
diff --git a/lint-checks/build.gradle b/lint-checks/build.gradle
index 470f379..5733dc1 100644
--- a/lint-checks/build.gradle
+++ b/lint-checks/build.gradle
@@ -23,6 +23,14 @@
id("kotlin")
}
+sourceSets {
+ // Pull integration test source code in for use by lint testing framework.
+ test.resources.srcDirs(
+ project(":lint-checks:integration-tests")
+ .projectDir.absolutePath + "/src/main"
+ )
+}
+
dependencies {
compileOnly(LINT_API_LATEST)
compileOnly(LINT_CHECKS_LATEST)
diff --git a/lint-checks/integration-tests/build.gradle b/lint-checks/integration-tests/build.gradle
index 825602b..50a9b49 100644
--- a/lint-checks/integration-tests/build.gradle
+++ b/lint-checks/integration-tests/build.gradle
@@ -17,22 +17,31 @@
import androidx.build.BuildOnServerKt
import androidx.build.dependencyTracker.AffectedModuleDetector
import androidx.build.uptodatedness.EnableCachingKt
-import org.apache.tools.ant.filters.ReplaceTokens
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import static androidx.build.dependencies.DependenciesKt.KOTLIN_STDLIB
plugins {
id("AndroidXPlugin")
id("com.android.library")
- id("AndroidXUiPlugin")
- id("org.jetbrains.kotlin.android")
+ id("kotlin-android")
}
dependencies {
- implementation("androidx.annotation:annotation:1.0.0")
+ implementation(projectOrArtifact(":annotation:annotation"))
implementation(KOTLIN_STDLIB)
}
+// Allow usage of Kotlin's @Experimental and @RequiresOptIn annotations.
+tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ freeCompilerArgs += [
+ "-Xuse-experimental=kotlin.Experimental",
+ "-Xopt-in=kotlin.RequiresOptIn",
+ ]
+ }
+}
+
androidx {
name = "Lint Checks Integration Tests"
description = "This is a sample library for confirming that lint checks execute correctly, b/177437928"
diff --git a/lint-checks/integration-tests/expected-lint-results.xml b/lint-checks/integration-tests/expected-lint-results.xml
index 7dd7360..e8f2901 100644
--- a/lint-checks/integration-tests/expected-lint-results.xml
+++ b/lint-checks/integration-tests/expected-lint-results.xml
@@ -2,54 +2,6 @@
<issues format="5" by="lint 4.2.0-beta04">
<issue
- id="UnknownIssueId"
- severity="Ignore"
- message="Unknown issue id "ComposableLambdaParameterNaming""
- category="Lint"
- priority="1"
- summary="Unknown Lint Issue Id"
- explanation="Lint will report this issue if it is configured with an issue id it does not recognize in for example Gradle files or `lint.xml` configuration files.">
- <location
- file="$SUPPORT/lint-checks/integration-tests/build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- severity="Ignore"
- message="Unknown issue id "ComposableLambdaParameterPosition""
- category="Lint"
- priority="1"
- summary="Unknown Lint Issue Id"
- explanation="Lint will report this issue if it is configured with an issue id it does not recognize in for example Gradle files or `lint.xml` configuration files.">
- <location
- file="$SUPPORT/lint-checks/integration-tests/build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- severity="Ignore"
- message="Unknown issue id "ComposableNaming""
- category="Lint"
- priority="1"
- summary="Unknown Lint Issue Id"
- explanation="Lint will report this issue if it is configured with an issue id it does not recognize in for example Gradle files or `lint.xml` configuration files.">
- <location
- file="$SUPPORT/lint-checks/integration-tests/build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- severity="Ignore"
- message="Unknown issue id "CompositionLocalNaming""
- category="Lint"
- priority="1"
- summary="Unknown Lint Issue Id"
- explanation="Lint will report this issue if it is configured with an issue id it does not recognize in for example Gradle files or `lint.xml` configuration files.">
- <location
- file="$SUPPORT/lint-checks/integration-tests/build.gradle"/>
- </issue>
-
- <issue
id="BanConcurrentHashMap"
severity="Error"
message="Detected ConcurrentHashMap usage."
@@ -60,28 +12,12 @@
errorLine1="import java.util.concurrent.ConcurrentHashMap;"
errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
- file="$SUPPORT/lint-checks/integration-tests/src/main/java/Sample.java"
+ file="$SUPPORT/lint-checks/integration-tests/src/main/java/androidx/Sample.java"
line="19"
column="1"/>
</issue>
<issue
- id="UnnecessaryLambdaCreation"
- severity="Error"
- message="Creating an unnecessary lambda to emit a captured lambda"
- category="Performance"
- priority="5"
- summary="Creating an unnecessary lambda to emit a captured lambda"
- explanation="Creating this extra lambda instead of just passing the already captured lambda means that during code generation the Compose compiler will insert code around this lambda to track invalidations. This adds some extra runtime cost so you should instead just directly pass the lambda as a parameter to the function."
- errorLine1=" lambda()"
- errorLine2=" ~~~~~~">
- <location
- file="$SUPPORT/lint-checks/integration-tests/src/main/java/ComposeSample.kt"
- line="29"
- column="9"/>
- </issue>
-
- <issue
id="UnknownNullness"
severity="Fatal"
message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
@@ -94,8 +30,8 @@
errorLine1=" public static Sample confirmIntrinisicLintChecksRun() {"
errorLine2=" ~~~~~~">
<location
- file="$SUPPORT/lint-checks/integration-tests/src/main/java/Sample.java"
- line="28"
+ file="$SUPPORT/lint-checks/integration-tests/src/main/java/androidx/Sample.java"
+ line="32"
column="19"/>
</issue>
@@ -112,8 +48,8 @@
errorLine1=" public static void confirmCustomAndroidXChecksRun(ConcurrentHashMap m) {"
errorLine2=" ~~~~~~~~~~~~~~~~~">
<location
- file="$SUPPORT/lint-checks/integration-tests/src/main/java/Sample.java"
- line="37"
+ file="$SUPPORT/lint-checks/integration-tests/src/main/java/androidx/Sample.java"
+ line="41"
column="55"/>
</issue>
diff --git a/lint-checks/integration-tests/src/main/java/ComposeSample.kt b/lint-checks/integration-tests/src/main/java/androidx/ComposeSample.kt
similarity index 96%
rename from lint-checks/integration-tests/src/main/java/ComposeSample.kt
rename to lint-checks/integration-tests/src/main/java/androidx/ComposeSample.kt
index c3e2c63..ca74815 100644
--- a/lint-checks/integration-tests/src/main/java/ComposeSample.kt
+++ b/lint-checks/integration-tests/src/main/java/androidx/ComposeSample.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("unused")
+
package androidx
fun lambdaFunction(lambda: () -> Unit) {
diff --git a/lint-checks/integration-tests/src/main/java/Sample.java b/lint-checks/integration-tests/src/main/java/androidx/Sample.java
similarity index 88%
rename from lint-checks/integration-tests/src/main/java/Sample.java
rename to lint-checks/integration-tests/src/main/java/androidx/Sample.java
index 5d2ba27..7c396ca 100644
--- a/lint-checks/integration-tests/src/main/java/Sample.java
+++ b/lint-checks/integration-tests/src/main/java/androidx/Sample.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,10 @@
import java.util.concurrent.ConcurrentHashMap;
+/**
+ * Sample class used to verify that ConcurrentHashMap lint check is running.
+ */
+@SuppressWarnings("unused")
public class Sample {
/**
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt b/lint-checks/integration-tests/src/main/java/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt
similarity index 61%
copy from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
copy to lint-checks/integration-tests/src/main/java/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt
index 7e01354..ded25ef 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
+++ b/lint-checks/integration-tests/src/main/java/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt
@@ -1,5 +1,3 @@
-// ktlint-disable filename
-
/*
* Copyright 2021 The Android Open Source Project
*
@@ -16,6 +14,17 @@
* limitations under the License.
*/
-package androidx.compose.foundation
+@file:Suppress("unused")
-internal actual typealias AtomicReference<V> = java.util.concurrent.atomic.AtomicReference<V>
\ No newline at end of file
+package androidx.sample.consumer
+
+import sample.annotation.provider.ExperimentalSampleAnnotation
+import sample.annotation.provider.ExperimentalSampleAnnotationJava
+
+class OutsideGroupExperimentalAnnotatedClass {
+ @ExperimentalSampleAnnotationJava
+ @ExperimentalSampleAnnotation
+ fun invalidAnnotatedMethod() {
+ // Nothing to see here.
+ }
+}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt b/lint-checks/integration-tests/src/main/java/sample/annotation/provider/ExperimentalSampleAnnotation.kt
similarity index 75%
copy from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
copy to lint-checks/integration-tests/src/main/java/sample/annotation/provider/ExperimentalSampleAnnotation.kt
index 7e01354..265455c 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
+++ b/lint-checks/integration-tests/src/main/java/sample/annotation/provider/ExperimentalSampleAnnotation.kt
@@ -1,5 +1,3 @@
-// ktlint-disable filename
-
/*
* Copyright 2021 The Android Open Source Project
*
@@ -16,6 +14,9 @@
* limitations under the License.
*/
-package androidx.compose.foundation
+package sample.annotation.provider
-internal actual typealias AtomicReference<V> = java.util.concurrent.atomic.AtomicReference<V>
\ No newline at end of file
+@RequiresOptIn
+@Retention(AnnotationRetention.BINARY)
+@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+annotation class ExperimentalSampleAnnotation
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt b/lint-checks/integration-tests/src/main/java/sample/annotation/provider/ExperimentalSampleAnnotationJava.java
similarity index 61%
copy from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
copy to lint-checks/integration-tests/src/main/java/sample/annotation/provider/ExperimentalSampleAnnotationJava.java
index 7e01354..cf47d87 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
+++ b/lint-checks/integration-tests/src/main/java/sample/annotation/provider/ExperimentalSampleAnnotationJava.java
@@ -1,5 +1,3 @@
-// ktlint-disable filename
-
/*
* Copyright 2021 The Android Open Source Project
*
@@ -16,6 +14,17 @@
* limitations under the License.
*/
-package androidx.compose.foundation
+package sample.annotation.provider;
-internal actual typealias AtomicReference<V> = java.util.concurrent.atomic.AtomicReference<V>
\ No newline at end of file
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import kotlin.RequiresOptIn;
+
+@RequiresOptIn
+@Retention(CLASS)
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface ExperimentalSampleAnnotationJava {}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt b/lint-checks/integration-tests/src/main/java/sample/annotation/provider/WithinGroupExperimentalAnnotatedClass.kt
similarity index 73%
copy from compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
copy to lint-checks/integration-tests/src/main/java/sample/annotation/provider/WithinGroupExperimentalAnnotatedClass.kt
index 7e01354..f36c34fd 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ActualDesktop.kt
+++ b/lint-checks/integration-tests/src/main/java/sample/annotation/provider/WithinGroupExperimentalAnnotatedClass.kt
@@ -1,5 +1,3 @@
-// ktlint-disable filename
-
/*
* Copyright 2021 The Android Open Source Project
*
@@ -16,6 +14,13 @@
* limitations under the License.
*/
-package androidx.compose.foundation
+@file:Suppress("unused")
-internal actual typealias AtomicReference<V> = java.util.concurrent.atomic.AtomicReference<V>
\ No newline at end of file
+package sample.annotation.provider
+
+class WithinGroupExperimentalAnnotatedClass {
+ @ExperimentalSampleAnnotation
+ fun validAnnotatedMethod() {
+ // Nothing to see here.
+ }
+}
diff --git a/lint-checks/lint-baseline.xml b/lint-checks/lint-baseline.xml
deleted file mode 100644
index 297ae16..0000000
--- a/lint-checks/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" version="4.2.0-beta02">
-
-</issues>
diff --git a/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt b/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
index e596f53..5bc2e75 100644
--- a/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
@@ -14,9 +14,11 @@
* limitations under the License.
*/
+@file:Suppress("UnstableApiUsage")
+
package androidx.build.lint
-import com.android.tools.lint.detector.api.AnnotationUsageType
+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
@@ -24,107 +26,93 @@
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.PsiElement
-import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiCompiledElement
+import org.jetbrains.uast.UAnnotated
import org.jetbrains.uast.UAnnotation
import org.jetbrains.uast.UElement
-import org.jetbrains.uast.getContainingUClass
+import org.jetbrains.uast.resolveToUElement
-class BanInappropriateExperimentalUsage : Detector(), SourceCodeScanner {
- override fun applicableAnnotations(): List<String>? = listOf(
- JAVA_EXPERIMENTAL_ANNOTATION,
- KOTLIN_OPT_IN_ANNOTATION,
- KOTLIN_EXPERIMENTAL_ANNOTATION
- )
+/**
+ * Prevents usage of experimental annotations outside the groups in which they were defined.
+ */
+class BanInappropriateExperimentalUsage : Detector(), Detector.UastScanner {
- override fun visitAnnotationUsage(
- context: JavaContext,
- usage: UElement,
- type: AnnotationUsageType,
- annotation: UAnnotation,
- qualifiedName: String,
- method: PsiMethod?,
- referenced: PsiElement?,
- annotations: List<UAnnotation>,
- allMemberAnnotations: List<UAnnotation>,
- allClassAnnotations: List<UAnnotation>,
- allPackageAnnotations: List<UAnnotation>
- ) {
- when (qualifiedName) {
- JAVA_EXPERIMENTAL_ANNOTATION,
- JAVA_OPT_IN_ANNOTATION,
- KOTLIN_EXPERIMENTAL_ANNOTATION,
- KOTLIN_OPT_IN_ANNOTATION -> {
- verifyExperimentalOrOptInUsageIsWithinSameGroup(
- context, usage, annotation
- )
+ override fun getApplicableUastTypes() = listOf(UAnnotation::class.java)
+
+ override fun createUastHandler(context: JavaContext): UElementHandler {
+ return AnnotationChecker(context)
+ }
+
+ private inner class AnnotationChecker(val context: JavaContext) : UElementHandler() {
+ override fun visitAnnotation(node: UAnnotation) {
+ val annotation = node.resolveToUElement()
+ if (annotation is UAnnotated) {
+ val annotations = context.evaluator.getAllAnnotations(annotation, false)
+ val isOptIn = annotations.any { APPLICABLE_ANNOTATIONS.contains(it.qualifiedName) }
+ if (isOptIn) {
+ verifyUsageOfElementIsWithinSameGroup(context, node, annotation, ISSUE)
+ }
}
}
}
- fun verifyExperimentalOrOptInUsageIsWithinSameGroup(
+ fun verifyUsageOfElementIsWithinSameGroup(
context: JavaContext,
usage: UElement,
- annotation: UAnnotation
+ annotation: UElement,
+ issue: Issue,
) {
- val declaringGroup = getApproximateAnnotationMavenGroup(annotation)
- val usingGroup = getApproximateUsageSiteMavenGroup(usage)
- // Don't flag if group is null for some reason (for now at least)
- // Also exclude sample for now, since it doesn't work well with our workaround (includes
- // class)
- if (declaringGroup != null && usingGroup != null && declaringGroup != usingGroup &&
- usingGroup != "sample"
- ) {
+ val evaluator = context.evaluator
+ val usageCoordinates = evaluator.getLibrary(usage) ?: context.project.mavenCoordinate
+ val annotationCoordinates = evaluator.getLibrary(annotation) ?: run {
+ // Is the annotation defined in source code?
+ if (usageCoordinates != null && annotation !is PsiCompiledElement) {
+ annotation.sourcePsi?.let { sourcePsi ->
+ evaluator.getProject(sourcePsi)?.mavenCoordinate
+ }
+ } else {
+ null
+ }
+ }
+ val usageGroupId = usageCoordinates?.groupId
+ val annotationGroupId = annotationCoordinates?.groupId
+ if (annotationGroupId != usageGroupId && annotationGroupId != null) {
context.report(
- BanInappropriateExperimentalUsage.ISSUE, usage, context.getNameLocation(usage),
- "`Experimental`/`OptIn` APIs should only be used from within the same library " +
- "or libraries within the same requireSameVersion group"
+ issue, usage, context.getNameLocation(usage),
+ "`Experimental` and `RequiresOptIn` APIs may only be used within the same-version" +
+ " group where they were defined."
)
}
}
- fun getApproximateAnnotationMavenGroup(annotation: UAnnotation): String? {
- if (annotation.getContainingUClass() == null || annotation.getContainingUClass()!!
- .qualifiedName == null
- ) {
- return null
- }
- return annotation.getContainingUClass()!!.qualifiedName!!.split(".").subList(0, 2)
- .joinToString(".")
- }
-
- fun getApproximateUsageSiteMavenGroup(usage: UElement): String? {
- if (usage.getContainingUClass() == null || usage.getContainingUClass()!!
- .qualifiedName == null
- ) {
- return null
- }
- return usage.getContainingUClass()!!.qualifiedName!!.split(".").subList(0, 2)
- .joinToString(".")
- }
-
companion object {
-
private const val KOTLIN_EXPERIMENTAL_ANNOTATION = "kotlin.Experimental"
-
+ private const val KOTLIN_REQUIRES_OPT_IN_ANNOTATION = "kotlin.RequiresOptIn"
private const val JAVA_EXPERIMENTAL_ANNOTATION =
"androidx.annotation.experimental.Experimental"
+ private const val JAVA_REQUIRES_OPT_IN_ANNOTATION =
+ "androidx.annotation.RequiresOptIn"
- private const val KOTLIN_OPT_IN_ANNOTATION =
- "kotlin.OptIn"
-
- private const val JAVA_OPT_IN_ANNOTATION =
- "androidx.annotation.OptIn"
+ private val APPLICABLE_ANNOTATIONS = listOf(
+ JAVA_EXPERIMENTAL_ANNOTATION,
+ KOTLIN_EXPERIMENTAL_ANNOTATION,
+ JAVA_REQUIRES_OPT_IN_ANNOTATION,
+ KOTLIN_REQUIRES_OPT_IN_ANNOTATION,
+ )
val ISSUE = Issue.create(
- "IllegalExperimentalApiUsage",
- "Using experimental api from separately versioned library",
- "APIs annotated with `@RequiresOptIn` or `@Experimental` are considered alpha." +
- "A caller from another library may not use them unless that the two libraries " +
- "are part of the same maven group and that group specifies requireSameVersion",
- Category.CORRECTNESS, 5, Severity.ERROR,
- Implementation(BanInappropriateExperimentalUsage::class.java, Scope.JAVA_FILE_SCOPE)
+ id = "IllegalExperimentalApiUsage",
+ briefDescription = "Using experimental API from separately versioned library",
+ explanation = "Annotations meta-annotated with `@RequiresOptIn` or `@Experimental` " +
+ "may only be referenced from within the same-version group in which they were " +
+ "defined.",
+ category = Category.CORRECTNESS,
+ priority = 5,
+ severity = Severity.ERROR,
+ implementation = Implementation(
+ BanInappropriateExperimentalUsage::class.java,
+ Scope.JAVA_FILE_SCOPE,
+ ),
)
}
-}
+}
\ No newline at end of file
diff --git a/lint-checks/src/test/java/androidx/build/lint/BanInappropriateExperimentalUsageTest.kt b/lint-checks/src/test/java/androidx/build/lint/BanInappropriateExperimentalUsageTest.kt
new file mode 100644
index 0000000..cd7a7b3
--- /dev/null
+++ b/lint-checks/src/test/java/androidx/build/lint/BanInappropriateExperimentalUsageTest.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.lint
+
+import com.android.tools.lint.checks.infrastructure.ProjectDescription
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.checks.infrastructure.TestFiles.gradle
+import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@Suppress("UnstableApiUsage")
+@RunWith(JUnit4::class)
+class BanInappropriateExperimentalUsageTest {
+
+ @Test
+ fun `Test within-module Experimental usage via Gradle model`() {
+ val provider = project()
+ .name("provider")
+ .files(
+ ktSample("sample.annotation.provider.WithinGroupExperimentalAnnotatedClass"),
+ ktSample("sample.annotation.provider.ExperimentalSampleAnnotation"),
+ gradle(
+ """
+ apply plugin: 'com.android.library'
+ group=sample.annotation.provider
+ """
+ ).indented(),
+ OPT_IN_KT,
+ )
+
+ lint()
+ .projects(provider)
+ .issues(BanInappropriateExperimentalUsage.ISSUE)
+ .run()
+ .expect(
+ """
+ No warnings.
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ fun `Test cross-module Experimental usage via Gradle model`() {
+ val provider = project()
+ .name("provider")
+ .report(false)
+ .files(
+ ktSample("sample.annotation.provider.ExperimentalSampleAnnotation"),
+ javaSample("sample.annotation.provider.ExperimentalSampleAnnotationJava"),
+ gradle(
+ """
+ apply plugin: 'com.android.library'
+ group=sample.annotation.provider
+ """
+ ).indented(),
+ OPT_IN_KT,
+ )
+
+ val consumer = project()
+ .name("consumer")
+ .dependsOn(provider)
+ .files(
+ ktSample("androidx.sample.consumer.OutsideGroupExperimentalAnnotatedClass"),
+ gradle(
+ """
+ apply plugin: 'com.android.library'
+ group=androidx.sample.consumer
+ """
+ ).indented()
+ )
+
+ lint()
+ .projects(provider, consumer)
+ .issues(BanInappropriateExperimentalUsage.ISSUE)
+ .run()
+ .expect(
+ """
+ consumer/src/main/kotlin/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt:25: Error: Experimental and RequiresOptIn APIs may only be used within the same-version group where they were defined. [IllegalExperimentalApiUsage]
+ @ExperimentalSampleAnnotationJava
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.trimIndent()
+ )
+ }
+
+ private fun project(): ProjectDescription = ProjectDescription()
+
+ /**
+ * Loads a [TestFile] from Java source code included in the JAR resources.
+ */
+ private fun javaSample(className: String): TestFile = TestFiles.java(
+ javaClass.getResource("/java/${className.replace('.', '/')}.java").readText()
+ )
+
+ /**
+ * Loads a [TestFile] from Kotlin source code included in the JAR resources.
+ */
+ private fun ktSample(className: String): TestFile = TestFiles.kotlin(
+ javaClass.getResource("/java/${className.replace('.', '/')}.kt").readText()
+ )
+}
+
+/* ktlint-disable max-line-length */
+
+/**
+ * [TestFile] containing OptIn.kt from the Kotlin standard library.
+ *
+ * This is a workaround for the Kotlin standard library used by the Lint test harness not
+ * including the Experimental annotation by default.
+ */
+private val OPT_IN_KT: TestFile = TestFiles.kotlin(
+ """
+ package kotlin
+
+ import kotlin.annotation.AnnotationRetention.BINARY
+ import kotlin.annotation.AnnotationRetention.SOURCE
+ import kotlin.annotation.AnnotationTarget.*
+ import kotlin.internal.RequireKotlin
+ import kotlin.internal.RequireKotlinVersionKind
+ import kotlin.reflect.KClass
+
+ @Target(ANNOTATION_CLASS)
+ @Retention(BINARY)
+ @SinceKotlin("1.3")
+ @RequireKotlin("1.3.70", versionKind = RequireKotlinVersionKind.COMPILER_VERSION)
+ public annotation class RequiresOptIn(
+ val message: String = "",
+ val level: Level = Level.ERROR
+ ) {
+ public enum class Level {
+ WARNING,
+ ERROR,
+ }
+ }
+
+ @Target(
+ CLASS, PROPERTY, LOCAL_VARIABLE, VALUE_PARAMETER, CONSTRUCTOR, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, EXPRESSION, FILE, TYPEALIAS
+ )
+ @Retention(SOURCE)
+ @SinceKotlin("1.3")
+ @RequireKotlin("1.3.70", versionKind = RequireKotlinVersionKind.COMPILER_VERSION)
+ public annotation class OptIn(
+ vararg val markerClass: KClass<out Annotation>
+ )
+ """.trimIndent()
+)
+
+/* ktlint-enable max-line-length */
diff --git a/media/version-compat-tests/previous/client/src/androidTest/java/android/support/mediacompat/client/RemoteUserInfoWithMediaControllerCompatTest.java b/media/version-compat-tests/previous/client/src/androidTest/java/android/support/mediacompat/client/RemoteUserInfoWithMediaControllerCompatTest.java
index 0567306..91fb0c3 100644
--- a/media/version-compat-tests/previous/client/src/androidTest/java/android/support/mediacompat/client/RemoteUserInfoWithMediaControllerCompatTest.java
+++ b/media/version-compat-tests/previous/client/src/androidTest/java/android/support/mediacompat/client/RemoteUserInfoWithMediaControllerCompatTest.java
@@ -48,6 +48,7 @@
import android.view.KeyEvent;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.FlakyTest;
import androidx.test.filters.LargeTest;
import org.junit.After;
@@ -64,6 +65,7 @@
* {@link MediaControllerCompat} methods.
*/
@RunWith(AndroidJUnit4.class)
+@FlakyTest(bugId = 182271958)
@LargeTest
public class RemoteUserInfoWithMediaControllerCompatTest {
private static final String TAG = "RemoteUserInfoCompat";
diff --git a/media2/media2-session/src/androidTest/java/androidx/media2/session/MediaBrowserTest.java b/media2/media2-session/src/androidTest/java/androidx/media2/session/MediaBrowserTest.java
deleted file mode 100644
index 95d3a9f..0000000
--- a/media2/media2-session/src/androidTest/java/androidx/media2/session/MediaBrowserTest.java
+++ /dev/null
@@ -1,533 +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.media2.session;
-
-import static androidx.media2.session.LibraryResult.RESULT_ERROR_PERMISSION_DENIED;
-import static androidx.media2.session.LibraryResult.RESULT_SUCCESS;
-import static androidx.media2.session.TestUtils.assertLibraryParamsEquals;
-import static androidx.media2.session.TestUtils.assertMediaItemEquals;
-import static androidx.media2.session.TestUtils.assertMediaItemWithId;
-import static androidx.media2.session.TestUtils.createLibraryParams;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertNull;
-import static junit.framework.Assert.assertTrue;
-import static junit.framework.Assert.fail;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-
-import android.os.Bundle;
-import android.os.Process;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.media2.session.MediaBrowser.BrowserCallback;
-import androidx.media2.session.MediaController.ControllerCallback;
-import androidx.media2.session.MediaLibraryService.LibraryParams;
-import androidx.media2.session.MediaLibraryService.MediaLibrarySession;
-import androidx.media2.session.MediaLibraryService.MediaLibrarySession.MediaLibrarySessionCallback;
-import androidx.media2.session.MediaSession.ControllerInfo;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.lang.reflect.Method;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * Tests {@link MediaBrowser}.
- * <p>
- * This test inherits {@link MediaControllerTest} to ensure that inherited APIs from
- * {@link MediaController} works cleanly.
- */
-// TODO(jaewan): Implement host-side test so browser and service can run in different processes.
-@RunWith(AndroidJUnit4.class)
-@LargeTest
-public class MediaBrowserTest extends MediaControllerTest {
- private static final String TAG = "MediaBrowserTest";
-
- @Override
- MediaController onCreateController(@NonNull final SessionToken token,
- @Nullable final Bundle connectionHints, @Nullable final TestBrowserCallback callback)
- throws InterruptedException {
- final AtomicReference<MediaController> controller = new AtomicReference<>();
- sHandler.postAndSync(new Runnable() {
- @Override
- public void run() {
- // Create controller on the test handler, for changing MediaBrowserCompat's Handler
- // Looper. Otherwise, MediaBrowserCompat will post all the commands to the handler
- // and commands wouldn't be run if tests codes waits on the test handler.
- MediaBrowser.Builder builder = new MediaBrowser.Builder(mContext)
- .setSessionToken(token)
- .setControllerCallback(sHandlerExecutor, callback);
- if (connectionHints != null) {
- builder.setConnectionHints(connectionHints);
- }
- controller.set(builder.build());
- }
- });
- return controller.get();
- }
-
- final MediaBrowser createBrowser() throws InterruptedException {
- return createBrowser(null);
- }
-
- final MediaBrowser createBrowser(@Nullable BrowserCallback callback)
- throws InterruptedException {
- return (MediaBrowser) createController(MockMediaLibraryService.getToken(mContext),
- true, null, callback);
- }
-
- /**
- * Test if the {@link TestBrowserCallback} wraps the callback proxy without missing any method.
- */
- @Test
- public void testBrowserCallback() {
- Method[] methods = TestBrowserCallback.class.getMethods();
- assertNotNull(methods);
- for (int i = 0; i < methods.length; i++) {
- // For any methods in the controller callback, TestBrowserCallback should have
- // overridden the method and call matching API in the callback proxy.
- assertNotEquals("TestBrowserCallback should override " + methods[i]
- + " and call callback proxy",
- BrowserCallback.class, methods[i].getDeclaringClass());
- assertNotEquals("TestBrowserCallback should override " + methods[i]
- + " and call callback proxy",
- ControllerCallback.class, methods[i].getDeclaringClass());
- }
- }
-
- @Test
- public void getLibraryRoot() throws Exception {
- final LibraryParams params = createLibraryParams();
-
- MockMediaLibraryService.setAssertLibraryParams(params);
- LibraryResult result = createBrowser().getLibraryRoot(params)
- .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
- assertEquals(RESULT_SUCCESS, result.getResultCode());
- assertMediaItemEquals(MockMediaLibraryService.ROOT_ITEM, result.getMediaItem());
- assertLibraryParamsEquals(MockMediaLibraryService.ROOT_PARAMS, result.getLibraryParams());
- }
-
- @Test
- public void getItem() throws Exception {
- final String mediaId = MockMediaLibraryService.MEDIA_ID_GET_ITEM;
-
- LibraryResult result = createBrowser().getItem(mediaId)
- .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
- assertEquals(RESULT_SUCCESS, result.getResultCode());
- assertMediaItemWithId(mediaId, result.getMediaItem());
- }
-
- @Test
- public void getItemNullResult() throws Exception {
- final String mediaId = "random_media_id";
-
- LibraryResult result = createBrowser().getItem(mediaId)
- .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
- assertNotEquals(RESULT_SUCCESS, result.getResultCode());
- assertNull(result.getMediaItem());
- }
-
- @Test
- public void getChildren() throws Exception {
- final String parentId = MockMediaLibraryService.PARENT_ID;
- final int page = 4;
- final int pageSize = 10;
- final LibraryParams params = createLibraryParams();
-
- MockMediaLibraryService.setAssertLibraryParams(params);
- LibraryResult result = createBrowser().getChildren(parentId, page, pageSize, params)
- .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
- assertEquals(RESULT_SUCCESS, result.getResultCode());
-
- TestUtils.assertPaginatedListEquals(MockMediaLibraryService.GET_CHILDREN_RESULT,
- page, pageSize, result.getMediaItems());
- }
-
- @Test
- public void getChildrenEmptyResult() throws Exception {
- final String parentId = MockMediaLibraryService.PARENT_ID_NO_CHILDREN;
-
- LibraryResult result = createBrowser().getChildren(parentId, 1, 1, null)
- .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
- assertEquals(RESULT_SUCCESS, result.getResultCode());
- assertEquals(0, result.getMediaItems().size());
- }
-
- @Test
- public void getChildrenNullResult() throws Exception {
- final String parentId = MockMediaLibraryService.PARENT_ID_ERROR;
-
- LibraryResult result = createBrowser().getChildren(parentId, 1, 1, null)
- .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
- assertNotEquals(RESULT_SUCCESS, result.getResultCode());
- assertNull(result.getMediaItems());
- }
-
- @Test
- public void search() throws Exception {
- final String query = MockMediaLibraryService.SEARCH_QUERY;
- final int page = 4;
- final int pageSize = 10;
- final LibraryParams params = createLibraryParams();
-
- final CountDownLatch latchForSearch = new CountDownLatch(1);
- final BrowserCallback callback = new BrowserCallback() {
- @Override
- public void onSearchResultChanged(MediaBrowser browser,
- String queryOut, int itemCount, LibraryParams paramsOut) {
- assertEquals(query, queryOut);
- assertLibraryParamsEquals(params, paramsOut);
- assertEquals(MockMediaLibraryService.SEARCH_RESULT_COUNT, itemCount);
- latchForSearch.countDown();
- }
- };
-
- // Request the search.
- MockMediaLibraryService.setAssertLibraryParams(params);
- MediaBrowser browser = createBrowser(callback);
- LibraryResult result = browser.search(query, params)
- .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
- assertEquals(RESULT_SUCCESS, result.getResultCode());
- assertTrue(latchForSearch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
-
- // Get the search result.
- result = browser.getSearchResult(query, page, pageSize, params)
- .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
- assertEquals(RESULT_SUCCESS, result.getResultCode());
-
- TestUtils.assertPaginatedListEquals(MockMediaLibraryService.SEARCH_RESULT,
- page, pageSize, result.getMediaItems());
- }
-
- @Test
- @LargeTest
- public void searchTakesTime() throws Exception {
- final String query = MockMediaLibraryService.SEARCH_QUERY_TAKES_TIME;
- final LibraryParams params = createLibraryParams();
-
- final CountDownLatch latch = new CountDownLatch(1);
- final BrowserCallback callback = new BrowserCallback() {
- @Override
- public void onSearchResultChanged(
- MediaBrowser browser, String queryOut, int itemCount,
- LibraryParams paramsOut) {
- assertEquals(query, queryOut);
- assertLibraryParamsEquals(params, paramsOut);
- assertEquals(MockMediaLibraryService.SEARCH_RESULT_COUNT, itemCount);
- latch.countDown();
- }
- };
-
- MockMediaLibraryService.setAssertLibraryParams(params);
- LibraryResult result = createBrowser(callback).search(query, params)
- .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
- assertEquals(RESULT_SUCCESS, result.getResultCode());
- assertTrue(latch.await(
- MockMediaLibraryService.SEARCH_TIME_IN_MS + TIMEOUT_MS, TimeUnit.MILLISECONDS));
- }
-
- @Test
- public void searchEmptyResult() throws Exception {
- final String query = MockMediaLibraryService.SEARCH_QUERY_EMPTY_RESULT;
- final LibraryParams params = createLibraryParams();
-
- final CountDownLatch latch = new CountDownLatch(1);
- final BrowserCallback callback = new BrowserCallback() {
- @Override
- public void onSearchResultChanged(MediaBrowser browser, String queryOut, int itemCount,
- LibraryParams paramsOut) {
- assertEquals(query, queryOut);
- assertLibraryParamsEquals(params, paramsOut);
- assertEquals(0, itemCount);
- latch.countDown();
- }
- };
-
- MockMediaLibraryService.setAssertLibraryParams(params);
- LibraryResult result = createBrowser(callback).search(query, params)
- .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
- assertEquals(RESULT_SUCCESS, result.getResultCode());
- assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
- }
-
- @Test
- public void subscribe() throws Exception {
- final String testParentId = "testSubscribeId";
- final LibraryParams params = createLibraryParams();
-
- final MediaLibrarySessionCallback callback = new MediaLibrarySessionCallback() {
- @Override
- public int onSubscribe(@NonNull MediaLibraryService.MediaLibrarySession session,
- @NonNull MediaSession.ControllerInfo info, @NonNull String parentId,
- @Nullable LibraryParams paramsOut) {
- if (Process.myUid() == info.getUid()) {
- assertEquals(testParentId, parentId);
- assertLibraryParamsEquals(params, paramsOut);
- return RESULT_SUCCESS;
- }
- return RESULT_ERROR_PERMISSION_DENIED;
- }
- };
- TestServiceRegistry.getInstance().setSessionCallback(callback);
- MockMediaLibraryService.setAssertLibraryParams(params);
- LibraryResult result = createBrowser().subscribe(testParentId, params)
- .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
- assertEquals(RESULT_SUCCESS, result.getResultCode());
- assertNull(result.getMediaItems());
- }
-
- @Test
- public void unsubscribe() throws Exception {
- final String testParentId = "testUnsubscribeId";
- final MediaLibrarySessionCallback callback = new MediaLibrarySessionCallback() {
- @Override
- public int onUnsubscribe(@NonNull MediaLibrarySession session,
- @NonNull ControllerInfo info, @NonNull String parentId) {
- if (Process.myUid() == info.getUid()) {
- assertEquals(testParentId, parentId);
- return RESULT_SUCCESS;
- }
- return RESULT_ERROR_PERMISSION_DENIED;
- }
- };
- TestServiceRegistry.getInstance().setSessionCallback(callback);
- LibraryResult result = createBrowser().unsubscribe(testParentId)
- .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
- assertEquals(RESULT_SUCCESS, result.getResultCode());
- assertNull(result.getMediaItems());
- }
-
- @Test
- public void browserCallback_onChildrenChangedIsNotCalledWhenNotSubscribed()
- throws Exception {
- // This test uses MediaLibrarySession.notifyChildrenChanged().
- final String subscribedMediaId = "subscribedMediaId";
- final String anotherMediaId = "anotherMediaId";
- final int testChildrenCount = 101;
-
- final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
- @Override
- public int onSubscribe(@NonNull MediaLibrarySession session,
- @NonNull ControllerInfo controller, @NonNull String parentId,
- @Nullable LibraryParams params) {
- if (Process.myUid() == controller.getUid()) {
- // Shouldn't trigger onChildrenChanged() for the browser,
- // because the browser didn't subscribe this media id.
- session.notifyChildrenChanged(anotherMediaId, testChildrenCount, null);
- }
- return RESULT_SUCCESS;
- }
-
- @NonNull
- @Override
- public LibraryResult onGetChildren(@NonNull MediaLibrarySession session,
- @NonNull ControllerInfo controller, @NonNull String parentId, int page,
- int pageSize, LibraryParams params) {
- // This wouldn't be called at all.
- return new LibraryResult(RESULT_SUCCESS,
- TestUtils.createMediaItems(testChildrenCount), null);
- }
- };
- final CountDownLatch latch = new CountDownLatch(1);
- final BrowserCallback controllerCallbackProxy = new BrowserCallback() {
- @Override
- public void onChildrenChanged(@NonNull MediaBrowser browser, @NonNull String parentId,
- int itemCount, LibraryParams params) {
- // Unexpected call.
- fail();
- latch.countDown();
- }
- };
-
- TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
- LibraryResult result = createBrowser(controllerCallbackProxy)
- .subscribe(subscribedMediaId, null)
- .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
- // Subscribe itself is success because onSubscribe() returned SUCCESS.
- assertEquals(RESULT_SUCCESS, result.getResultCode());
-
- // notifyChildrenChanged() in onSubscribe() should fail onChildrenChanged() should not be
- // called, because the ID hasn't been subscribed.
- assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
- }
-
- @Test
- public void browserCallback_onChildrenChangedIsCalledWhenSubscribed() throws Exception {
- // This test uses MediaLibrarySession.notifyChildrenChanged().
- final String expectedParentId = "expectedParentId";
- final int testChildrenCount = 101;
- final LibraryParams testParams = createLibraryParams();
-
- final CountDownLatch latch = new CountDownLatch(1);
- final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
- @Override
- public int onSubscribe(@NonNull MediaLibrarySession session,
- @NonNull ControllerInfo controller, @NonNull String parentId,
- @Nullable LibraryParams params) {
- if (Process.myUid() == controller.getUid()) {
- // Should trigger onChildrenChanged() for the browser.
- session.notifyChildrenChanged(expectedParentId, testChildrenCount, params);
- }
- return RESULT_SUCCESS;
- }
-
- @NonNull
- @Override
- public LibraryResult onGetChildren(@NonNull MediaLibrarySession session,
- @NonNull ControllerInfo controller, @NonNull String parentId, int page,
- int pageSize, LibraryParams params) {
- return new LibraryResult(RESULT_SUCCESS,
- TestUtils.createMediaItems(testChildrenCount), null);
- }
- };
- final BrowserCallback controllerCallbackProxy = new BrowserCallback() {
- @Override
- public void onChildrenChanged(@NonNull MediaBrowser browser, @NonNull String parentId,
- int itemCount, LibraryParams params) {
- assertEquals(expectedParentId, parentId);
- assertEquals(testChildrenCount, itemCount);
- assertLibraryParamsEquals(testParams, params);
- latch.countDown();
- }
- };
-
- TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
- MockMediaLibraryService.setAssertLibraryParams(testParams);
- LibraryResult result = createBrowser(controllerCallbackProxy)
- .subscribe(expectedParentId, testParams)
- .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
- assertEquals(RESULT_SUCCESS, result.getResultCode());
-
- // onChildrenChanged() should be called.
- assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
- }
-
- @Test
- public void browserCallback_onChildrenChangedIsNotCalledWhenNotSubscribed2()
- throws Exception {
- // This test uses MediaLibrarySession.notifyChildrenChanged(ControllerInfo).
- final String subscribedMediaId = "subscribedMediaId";
- final String anotherMediaId = "anotherMediaId";
- final int testChildrenCount = 101;
-
- final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
- @Override
- public int onSubscribe(@NonNull MediaLibrarySession session,
- @NonNull ControllerInfo controller, @NonNull String parentId,
- @Nullable LibraryParams params) {
- if (Process.myUid() == controller.getUid()) {
- // Shouldn't trigger onChildrenChanged() for the browser,
- // because the browser didn't subscribe this media id.
- session.notifyChildrenChanged(
- controller, anotherMediaId, testChildrenCount, null);
- }
- return RESULT_SUCCESS;
- }
-
- @NonNull
- @Override
- public LibraryResult onGetChildren(@NonNull MediaLibrarySession session,
- @NonNull ControllerInfo controller, @NonNull String parentId, int page,
- int pageSize, LibraryParams params) {
- return new LibraryResult(RESULT_SUCCESS,
- TestUtils.createMediaItems(testChildrenCount), null);
- }
- };
- final CountDownLatch latch = new CountDownLatch(1);
- final BrowserCallback controllerCallbackProxy = new BrowserCallback() {
- @Override
- public void onChildrenChanged(@NonNull MediaBrowser browser, @NonNull String parentId,
- int itemCount, LibraryParams params) {
- // Unexpected call.
- fail();
- latch.countDown();
- }
- };
-
- TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
-
- LibraryResult result = createBrowser(controllerCallbackProxy)
- .subscribe(subscribedMediaId, null)
- .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
-
- // onSubscribe() always returns SUCCESS, so success is expected.
- assertEquals(RESULT_SUCCESS, result.getResultCode());
-
- // But onChildrenChanged() wouldn't be called because notifyChildrenChanged() fails.
- assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
- }
-
- @Test
- public void browserCallback_onChildrenChangedIsCalledWhenSubscribed2() throws Exception {
- // This test uses MediaLibrarySession.notifyChildrenChanged(ControllerInfo).
- final String expectedParentId = "expectedParentId";
- final int testChildrenCount = 101;
- final LibraryParams testParams = createLibraryParams();
-
- final CountDownLatch latch = new CountDownLatch(1);
- final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
- @Override
- public int onSubscribe(@NonNull MediaLibrarySession session,
- @NonNull ControllerInfo controller, @NonNull String parentId,
- @Nullable LibraryParams params) {
- if (Process.myUid() == controller.getUid()) {
- // Should trigger onChildrenChanged() for the browser.
- session.notifyChildrenChanged(
- controller, expectedParentId, testChildrenCount, testParams);
- }
- return RESULT_SUCCESS;
- }
-
- @NonNull
- @Override
- public LibraryResult onGetChildren(@NonNull MediaLibrarySession session,
- @NonNull ControllerInfo controller, @NonNull String parentId, int page,
- int pageSize, LibraryParams params) {
- return new LibraryResult(RESULT_SUCCESS,
- TestUtils.createMediaItems(testChildrenCount), null);
- }
- };
- final BrowserCallback controllerCallbackProxy = new BrowserCallback() {
- @Override
- public void onChildrenChanged(@NonNull MediaBrowser browser, @NonNull String parentId,
- int itemCount, LibraryParams params) {
- assertEquals(expectedParentId, parentId);
- assertEquals(testChildrenCount, itemCount);
- assertLibraryParamsEquals(testParams, params);
- latch.countDown();
- }
- };
-
- TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
- LibraryResult result = createBrowser(controllerCallbackProxy)
- .subscribe(expectedParentId, null)
- .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
- assertEquals(RESULT_SUCCESS, result.getResultCode());
-
- // onChildrenChanged() should be called.
- assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
- }
-}
diff --git a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceTest.java b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceTest.java
index 6b8a632..50f0dcc 100644
--- a/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceTest.java
+++ b/media2/media2-session/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceTest.java
@@ -61,6 +61,7 @@
@Before
public void setUp() throws Exception {
super.setUp();
+ TestServiceRegistry.getInstance().cleanUp();
TestServiceRegistry.getInstance().setHandler(sHandler);
mToken = new SessionToken(mContext,
new ComponentName(mContext, MockMediaSessionService.class));
diff --git a/media2/media2-session/version-compat-tests/previous/client/src/androidTest/java/androidx/media2/test/client/tests/MediaBrowserCallbackTest.java b/media2/media2-session/version-compat-tests/previous/client/src/androidTest/java/androidx/media2/test/client/tests/MediaBrowserCallbackTest.java
index 38a4296..1ddc1c0 100644
--- a/media2/media2-session/version-compat-tests/previous/client/src/androidTest/java/androidx/media2/test/client/tests/MediaBrowserCallbackTest.java
+++ b/media2/media2-session/version-compat-tests/previous/client/src/androidTest/java/androidx/media2/test/client/tests/MediaBrowserCallbackTest.java
@@ -60,6 +60,7 @@
import androidx.test.filters.LargeTest;
import androidx.versionedparcelable.ParcelUtils;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -154,6 +155,7 @@
}
@Test
+ @Ignore("Throwing fatal exceptions: b/178708442")
public void getItem_nullResult() throws Exception {
prepareLooper();
final String mediaId = MediaBrowserConstants.MEDIA_ID_GET_NULL_ITEM;
@@ -175,6 +177,7 @@
}
@Test
+ @Ignore("Throwing fatal exceptions: b/178708442")
public void getItem_invalidResult() throws Exception {
prepareLooper();
final String mediaId = MediaBrowserConstants.MEDIA_ID_GET_INVALID_ITEM;
diff --git a/media2/media2-session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceTest.java b/media2/media2-session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceTest.java
index a251a80..0ec1ad4 100644
--- a/media2/media2-session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceTest.java
+++ b/media2/media2-session/version-compat-tests/previous/service/src/androidTest/java/androidx/media2/test/service/tests/MediaSessionServiceTest.java
@@ -60,6 +60,7 @@
@Before
public void setUp() throws Exception {
super.setUp();
+ TestServiceRegistry.getInstance().cleanUp();
TestServiceRegistry.getInstance().setHandler(sHandler);
mToken = new SessionToken(mContext,
new ComponentName(mContext, MockMediaSessionService.class));
diff --git a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouter2Test.java b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouter2Test.java
index 551219e..76cf79c 100644
--- a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouter2Test.java
+++ b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouter2Test.java
@@ -224,6 +224,7 @@
}
}
+ @FlakyTest // b/182205261
@SmallTest
@Test
public void setRouterParams_onRouteParamsChangedCalled() throws Exception {
@@ -233,8 +234,8 @@
addCallback(new MediaRouter.Callback() {
@Override
public void onRouterParamsChanged(MediaRouter router, MediaRouterParams params) {
- onRouterParmasChangedLatch.countDown();
routerParams[0] = params;
+ onRouterParmasChangedLatch.countDown();
}
});
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index ad1a40c..d384453 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -37,7 +37,7 @@
api(projectOrArtifact(":compose:runtime:runtime-saveable"))
api(projectOrArtifact(":compose:ui:ui"))
api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-compose"))
- api("androidx.navigation:navigation-runtime-ktx:2.3.4")
+ api(prebuiltOrSnapshot("androidx.navigation:navigation-runtime-ktx:2.3.4"))
androidTestImplementation(projectOrArtifact(":compose:material:material"))
androidTestImplementation("androidx.navigation:navigation-testing:2.3.1")
diff --git a/navigation/navigation-compose/integration-tests/navigation-demos/lint-baseline.xml b/navigation/navigation-compose/integration-tests/navigation-demos/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/navigation/navigation-compose/integration-tests/navigation-demos/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/navigation/navigation-compose/lint-baseline.xml b/navigation/navigation-compose/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/navigation/navigation-compose/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/navigation/navigation-compose/samples/lint-baseline.xml b/navigation/navigation-compose/samples/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/navigation/navigation-compose/samples/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/paging/common/lint-baseline.xml b/paging/common/lint-baseline.xml
deleted file mode 100644
index 297ae16..0000000
--- a/paging/common/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" version="4.2.0-beta02">
-
-</issues>
diff --git a/paging/paging-compose/integration-tests/paging-demos/lint-baseline.xml b/paging/paging-compose/integration-tests/paging-demos/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/paging/paging-compose/integration-tests/paging-demos/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/paging/paging-compose/lint-baseline.xml b/paging/paging-compose/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/paging/paging-compose/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/paging/paging-compose/samples/lint-baseline.xml b/paging/paging-compose/samples/lint-baseline.xml
deleted file mode 100644
index 53ae1f6..0000000
--- a/paging/paging-compose/samples/lint-baseline.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
-</issues>
diff --git a/placeholder-tests/lint-baseline.xml b/placeholder-tests/lint-baseline.xml
deleted file mode 100644
index 8f1aa4b..0000000
--- a/placeholder-tests/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/playground-common/playground-include-settings.gradle b/playground-common/playground-include-settings.gradle
index 4f2e033..994668d 100644
--- a/playground-common/playground-include-settings.gradle
+++ b/playground-common/playground-include-settings.gradle
@@ -65,6 +65,8 @@
}
settings.includeBuild(new File(supportRoot, "androidx-plugin"))
settings.includeProject(":lint-checks", new File(supportRoot, "lint-checks"))
+ settings.includeProject(":lint-checks:integration-tests",
+ new File(supportRoot, "lint-checks/integration-tests"))
settings.includeProject(":fakeannotations", new File(supportRoot,"fakeannotations"))
settings.includeProject(":internal-testutils-common",
new File(supportRoot, "testutils/testutils-common"))
diff --git a/preference/preference/build.gradle b/preference/preference/build.gradle
index 21edee3..5cc86e5 100644
--- a/preference/preference/build.gradle
+++ b/preference/preference/build.gradle
@@ -26,7 +26,7 @@
}
dependencies {
- api(project(":annotation:annotation"))
+ api("androidx.annotation:annotation:1.2.0-rc01")
api("androidx.appcompat:appcompat:1.1.0")
// Use the latest version of core library for verifying insets visibility
api(project(":core:core"))
diff --git a/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt b/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt
index 8deb8c2..474c81d 100644
--- a/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt
+++ b/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt
@@ -23,6 +23,7 @@
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
import androidx.test.filters.MediumTest
import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
@@ -130,6 +131,7 @@
}
@Test
+ @FlakyTest(bugId = 182343970)
public fun dbNotClosedWithRefCountIncremented() {
autoCloser.incrementCountAndEnsureDbIsOpen()
diff --git a/settings.gradle b/settings.gradle
index dfba23f..a728444 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -232,9 +232,12 @@
includeProject(":compose:animation", "compose/animation", [BuildType.COMPOSE])
includeProject(":compose:animation:animation", "compose/animation/animation", [BuildType.COMPOSE])
includeProject(":compose:animation:animation-core", "compose/animation/animation-core", [BuildType.COMPOSE])
+includeProject(":compose:animation:animation-core:animation-core-benchmark", "compose/animation/animation-core/benchmark", [BuildType.COMPOSE])
includeProject(":compose:animation:animation-core:animation-core-samples", "compose/animation/animation-core/samples", [BuildType.COMPOSE])
includeProject(":compose:animation:animation:integration-tests:animation-demos", "compose/animation/animation/integration-tests/animation-demos", [BuildType.COMPOSE])
includeProject(":compose:animation:animation:animation-samples", "compose/animation/animation/samples", [BuildType.COMPOSE])
+includeProject(":compose:benchmark-utils", "compose/benchmark-utils", [BuildType.COMPOSE])
+includeProject(":compose:benchmark-utils:benchmark-utils-benchmark", "compose/benchmark-utils/benchmark", [BuildType.COMPOSE])
includeProject(":compose:compiler:compiler", "compose/compiler/compiler", [BuildType.COMPOSE])
includeProject(":compose:compiler:compiler-hosted", "compose/compiler/compiler-hosted", [BuildType.COMPOSE])
includeProject(":compose:compiler:compiler-hosted:integration-tests", "compose/compiler/compiler-hosted/integration-tests", [BuildType.COMPOSE])
@@ -290,15 +293,20 @@
includeProject(":compose:test-utils", "compose/test-utils", [BuildType.COMPOSE])
includeProject(":compose:ui", "compose/ui", [BuildType.COMPOSE])
includeProject(":compose:ui:ui", "compose/ui/ui", [BuildType.COMPOSE])
+includeProject(":compose:ui:ui-benchmark", "compose/ui/ui/benchmark", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-android-stubs", "compose/ui/ui-android-stubs", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-geometry", "compose/ui/ui-geometry", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-graphics", "compose/ui/ui-graphics", [BuildType.COMPOSE])
+includeProject(":compose:ui:ui-graphics:ui-graphics-benchmark", "compose/ui/ui-graphics/benchmark", [BuildType.COMPOSE])
+includeProject(":compose:ui:ui-graphics:ui-graphics-benchmark:test", "compose/ui/ui-graphics/benchmark/test", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-graphics:ui-graphics-samples", "compose/ui/ui-graphics/samples", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-inspection", "compose/ui/ui-inspection", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-lint", "compose/ui/ui-lint", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-test", "compose/ui/ui-test", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-test-font", "compose/ui/ui-test-font", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-test-junit4", "compose/ui/ui-test-junit4", [BuildType.COMPOSE])
+includeProject(":compose:ui:ui-test-manifest", "compose/ui/ui-test-manifest", [BuildType.COMPOSE])
+includeProject(":compose:ui:ui-test-manifest:integration-tests:testapp", "compose/ui/ui-test-manifest/integration-tests/testapp", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-text", "compose/ui/ui-text", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-text:ui-text-benchmark", "compose/ui/ui-text/benchmark", [BuildType.COMPOSE])
includeProject(":compose:ui:ui-text:ui-text-samples", "compose/ui/ui-text/samples", [BuildType.COMPOSE])
@@ -350,6 +358,8 @@
includeProject(":emoji-bundled", "emoji/bundled", [BuildType.MAIN])
includeProject(":emoji2:emoji2", "emoji2/emoji2", [BuildType.MAIN])
includeProject(":emoji2:emoji2-bundled", "emoji2/emoji2-bundled", [BuildType.MAIN])
+includeProject(":emoji2:emoji2-views", "emoji2/emoji2-views", [BuildType.MAIN])
+includeProject(":emoji2:emoji2-views-core", "emoji2/emoji2-views-core", [BuildType.MAIN])
includeProject(":enterprise-feedback", "enterprise/feedback", [BuildType.MAIN])
includeProject(":enterprise-feedback-testing", "enterprise/feedback/testing", [BuildType.MAIN])
includeProject(":exifinterface:exifinterface", "exifinterface/exifinterface", [BuildType.MAIN])
@@ -414,8 +424,8 @@
includeProject(":lifecycle:lifecycle-viewmodel-ktx", "lifecycle/lifecycle-viewmodel-ktx", [BuildType.MAIN, BuildType.FLAN])
includeProject(":lifecycle:lifecycle-viewmodel-savedstate", "lifecycle/lifecycle-viewmodel-savedstate", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR])
includeProject(":lint-checks", "lint-checks")
-includeProject(":lint-checks:integration-tests", "lint-checks/integration-tests", [BuildType.COMPOSE])
-includeProject(":lint-checks:tests", "lint-checks/tests", [BuildType.MAIN])
+includeProject(":lint-checks:integration-tests", "lint-checks/integration-tests")
+includeProject(":lint-checks:tests", "lint-checks/tests")
includeProject(":lint-demos:lint-demo-appcompat", "lint-demos/lint-demo-appcompat", [BuildType.MAIN])
includeProject(":loader:loader", "loader/loader", [BuildType.MAIN])
includeProject(":loader:loader-ktx", "loader/loader-ktx", [BuildType.MAIN])
@@ -560,7 +570,6 @@
includeProject(":wear:wear-phone-interactions", "wear/wear-phone-interactions", [BuildType.MAIN, BuildType.WEAR])
includeProject(":wear:wear-remote-interactions", "wear/wear-remote-interactions", [BuildType.MAIN, BuildType.WEAR])
includeProject(":wear:wear-tiles", "wear/wear-tiles", [BuildType.MAIN, BuildType.WEAR])
-includeProject(":wear:wear-tiles-data", "wear/wear-tiles-data", [BuildType.MAIN, BuildType.WEAR])
includeProject(":wear:wear-tiles-proto", "wear/wear-tiles-proto", [BuildType.MAIN, BuildType.WEAR])
includeProject(":wear:wear-tiles-renderer", "wear/wear-tiles-renderer", [BuildType.MAIN, BuildType.WEAR])
includeProject(":wear:wear-watchface", "wear/wear-watchface", [BuildType.MAIN, BuildType.WEAR])
@@ -573,6 +582,7 @@
includeProject(":wear:wear-watchface-editor-samples", "wear/wear-watchface-editor/samples", [BuildType.MAIN, BuildType.WEAR])
includeProject(":wear:wear-watchface-guava", "wear/wear-watchface/guava", [BuildType.MAIN, BuildType.WEAR])
includeProject(":wear:wear-watchface-samples", "wear/wear-watchface/samples", [BuildType.MAIN, BuildType.WEAR])
+includeProject(":wear:wear-watchface-samples-minimal", "wear/wear-watchface/samples/minimal", [BuildType.MAIN, BuildType.WEAR])
includeProject(":wear:wear-watchface-style", "wear/wear-watchface-style", [BuildType.MAIN, BuildType.WEAR])
includeProject(":webkit:integration-tests:testapp", "webkit/integration-tests/testapp", [BuildType.MAIN])
includeProject(":webkit:webkit", "webkit/webkit", [BuildType.MAIN])
diff --git a/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java b/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
index 7890aa4..3afe39e 100644
--- a/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
+++ b/slidingpanelayout/slidingpanelayout/src/main/java/androidx/slidingpanelayout/widget/SlidingPaneLayout.java
@@ -1704,7 +1704,7 @@
* @return A pair of rects define the position of the split, or {@null} if there is no split
*/
private ArrayList<Rect> splitViewPositions() {
- if (mFoldingFeature == null || !isValidFoldStateForSplit(mFoldingFeature)) {
+ if (mFoldingFeature == null || !mFoldingFeature.isSeparating()) {
return null;
}
@@ -1748,24 +1748,6 @@
return foldRectInView;
}
- private static boolean isValidFoldStateForSplit(@NonNull FoldingFeature foldingFeature) {
- // Only a fold or a hinge can split the view
- if (foldingFeature.getType() != FoldingFeature.TYPE_FOLD
- && foldingFeature.getType() != FoldingFeature.TYPE_HINGE) {
- return false;
- }
- // A foldable device with hinge is always separating
- if (foldingFeature.getType() == FoldingFeature.TYPE_HINGE) {
- return true;
- }
- // Split the view when a foldable device is in half opened state or flipped state
- if (foldingFeature.getState() != FoldingFeature.STATE_HALF_OPENED
- && foldingFeature.getState() != FoldingFeature.STATE_FLIPPED) {
- return false;
- }
- return true;
- }
-
/**
* A device folding feature observer is used to notify listener when there is a folding feature
* change.
diff --git a/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/InvalidationTest.kt b/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/InvalidationTest.kt
index 7918eea..ea6425a 100644
--- a/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/InvalidationTest.kt
+++ b/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/InvalidationTest.kt
@@ -48,6 +48,7 @@
val temporaryFolder = TemporaryFolder(getInstrumentation().context.cacheDir)
@Test
+ @FlakyTest(bugId = 159202455)
fun test_exec_hook_methods() = test_simple_hook_methods(
listOf(
"execute()V",
diff --git a/test/screenshot/lint-baseline.xml b/test/screenshot/lint-baseline.xml
deleted file mode 100644
index 8f1aa4b..0000000
--- a/test/screenshot/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/testutils/testutils-common/lint-baseline.xml b/testutils/testutils-common/lint-baseline.xml
deleted file mode 100644
index 297ae16..0000000
--- a/testutils/testutils-common/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" version="4.2.0-beta02">
-
-</issues>
diff --git a/testutils/testutils-ktx/lint-baseline.xml b/testutils/testutils-ktx/lint-baseline.xml
deleted file mode 100644
index 297ae16..0000000
--- a/testutils/testutils-ktx/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" version="4.2.0-beta02">
-
-</issues>
diff --git a/testutils/testutils-navigation/lint-baseline.xml b/testutils/testutils-navigation/lint-baseline.xml
deleted file mode 100644
index 8f1aa4b..0000000
--- a/testutils/testutils-navigation/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/testutils/testutils-paging/lint-baseline.xml b/testutils/testutils-paging/lint-baseline.xml
deleted file mode 100644
index 297ae16..0000000
--- a/testutils/testutils-paging/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" version="4.2.0-beta02">
-
-</issues>
diff --git a/testutils/testutils-runtime/lint-baseline.xml b/testutils/testutils-runtime/lint-baseline.xml
index 135f9c2..0b60048 100644
--- a/testutils/testutils-runtime/lint-baseline.xml
+++ b/testutils/testutils-runtime/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
+<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
<issue
id="BanUncheckedReflection"
diff --git a/text/text/lint-baseline.xml b/text/text/lint-baseline.xml
index 3022dcc..0a690c9 100644
--- a/text/text/lint-baseline.xml
+++ b/text/text/lint-baseline.xml
@@ -1,81 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableLambdaParameterPosition"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnknownIssueId"
- message="Unknown issue id "ComposableNaming"">
- <location
- file="build.gradle"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 28, the call containing class androidx.compose.ui.text.android.style.FontSpan is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
- errorLine1=" oldTypeface.weight"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/compose/ui/text/android/style/FontSpan.kt"
- line="46"
- column="25"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 28, the call containing class androidx.compose.ui.text.android.style.FontWeightStyleSpan is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
- errorLine1=" (weight != 0 && weight != oldTypeface?.weight)"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/compose/ui/text/android/style/FontWeightStyleSpan.kt"
- line="67"
- column="52"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 28, the call containing class androidx.compose.ui.text.android.style.FontWeightStyleSpan is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
- errorLine1=" oldTypeface?.weight ?: FontStyle.FONT_WEIGHT_NORMAL"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/compose/ui/text/android/style/FontWeightStyleSpan.kt"
- line="79"
- column="26"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 28, the call containing class androidx.compose.ui.text.android.style.FontWeightStyleSpan is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
- errorLine1=" textPaint.typeface = Typeface.create(oldTypeface, newWeight, newItalic)"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/compose/ui/text/android/style/FontWeightStyleSpan.kt"
- line="88"
- column="39"/>
- </issue>
-
- <issue
- id="UnsafeNewApiCall"
- message="This call is to a method from API 28, the call containing class androidx.compose.ui.text.android.style.FontWeightStyleSpan is not annotated with @RequiresApi(x) where x is at least 28. Either annotate the containing class with at least @RequiresApi(28) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(28)."
- errorLine1=" textPaint.typeface = Typeface.create(oldTypeface, newWeight, newItalic)"
- errorLine2=" ~~~~~~">
- <location
- file="src/main/java/androidx/compose/ui/text/android/style/FontWeightStyleSpan.kt"
- line="88"
- column="39"/>
- </issue>
+<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
<issue
id="UnsafeNewApiCall"
diff --git a/tracing/tracing-ktx/lint-baseline.xml b/tracing/tracing-ktx/lint-baseline.xml
deleted file mode 100644
index 8f1aa4b..0000000
--- a/tracing/tracing-ktx/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/tracing/tracing/lint-baseline.xml b/tracing/tracing/lint-baseline.xml
index 07252f3..3e52b9c 100644
--- a/tracing/tracing/lint-baseline.xml
+++ b/tracing/tracing/lint-baseline.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
+<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
<issue
id="UnsafeNewApiCall"
diff --git a/tracing/tracing/src/androidTest/java/androidx/tracing/TraceTest.java b/tracing/tracing/src/androidTest/java/androidx/tracing/TraceTest.java
index 1e24c25..b9f58ca 100644
--- a/tracing/tracing/src/androidTest/java/androidx/tracing/TraceTest.java
+++ b/tracing/tracing/src/androidTest/java/androidx/tracing/TraceTest.java
@@ -77,6 +77,7 @@
}
@Test
+ @SdkSuppress(minSdkVersion = 29) // SELinux
public void beginAndEndSectionAsync() throws IOException {
startTrace();
Trace.beginAsyncSection("beginAndEndSectionAsync", /*cookie=*/5099);
@@ -88,6 +89,7 @@
}
@Test
+ @SdkSuppress(minSdkVersion = 29) // SELinux
public void setCounter() throws IOException {
startTrace();
Trace.setCounter("counterName", 42);
diff --git a/ui/ui-animation-tooling-internal/lint-baseline.xml b/ui/ui-animation-tooling-internal/lint-baseline.xml
deleted file mode 100644
index 297ae16..0000000
--- a/ui/ui-animation-tooling-internal/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" version="4.2.0-beta02">
-
-</issues>
diff --git a/wear/wear-complications-data/api/current.txt b/wear/wear-complications-data/api/current.txt
index f9f2248..d1e2d37 100644
--- a/wear/wear-complications-data/api/current.txt
+++ b/wear/wear-complications-data/api/current.txt
@@ -231,6 +231,18 @@
enum_constant public static final androidx.wear.complications.data.ComplicationType SMALL_IMAGE;
}
+ public final class CountDownTimeReference {
+ ctor public CountDownTimeReference(long dateTimeMillis);
+ method public long getDateTimeMillis();
+ property public final long dateTimeMillis;
+ }
+
+ public final class CountUpTimeReference {
+ ctor public CountUpTimeReference(long dateTimeMillis);
+ method public long getDateTimeMillis();
+ property public final long dateTimeMillis;
+ }
+
public final class DataKt {
}
@@ -470,7 +482,8 @@
}
public static final class TimeDifferenceComplicationText.Builder {
- ctor public TimeDifferenceComplicationText.Builder(androidx.wear.complications.data.TimeDifferenceStyle style, androidx.wear.complications.data.TimeReference reference);
+ ctor public TimeDifferenceComplicationText.Builder(androidx.wear.complications.data.TimeDifferenceStyle style, androidx.wear.complications.data.CountUpTimeReference countUpTimeReference);
+ ctor public TimeDifferenceComplicationText.Builder(androidx.wear.complications.data.TimeDifferenceStyle style, androidx.wear.complications.data.CountDownTimeReference countDownTimeReference);
method public androidx.wear.complications.data.TimeDifferenceComplicationText build();
method public androidx.wear.complications.data.TimeDifferenceComplicationText.Builder setDisplayAsNow(boolean displayAsNow);
method public androidx.wear.complications.data.TimeDifferenceComplicationText.Builder setMinimumUnit(java.util.concurrent.TimeUnit? minimumUnit);
@@ -521,25 +534,6 @@
method public androidx.wear.complications.data.TimeRange between(long startDateTimeMillis, long endDateTimeMillis);
}
- public final class TimeReference {
- method public static androidx.wear.complications.data.TimeReference between(long startDateTimeMillis, long endDateTimeMillis);
- method public static androidx.wear.complications.data.TimeReference ending(long dateTimeMillis);
- method public long getEndDateTimeMillis();
- method public long getStartDateTimeMillis();
- method public boolean hasEndDateTimeMillis();
- method public boolean hasStartDateTimeMillis();
- method public static androidx.wear.complications.data.TimeReference starting(long dateTimeMillis);
- property public final long endDateTimeMillis;
- property public final long startDateTimeMillis;
- field public static final androidx.wear.complications.data.TimeReference.Companion Companion;
- }
-
- public static final class TimeReference.Companion {
- method public androidx.wear.complications.data.TimeReference between(long startDateTimeMillis, long endDateTimeMillis);
- method public androidx.wear.complications.data.TimeReference ending(long dateTimeMillis);
- method public androidx.wear.complications.data.TimeReference starting(long dateTimeMillis);
- }
-
public final class TypeKt {
}
diff --git a/wear/wear-complications-data/api/public_plus_experimental_current.txt b/wear/wear-complications-data/api/public_plus_experimental_current.txt
index 7d78c5c..9d6e270 100644
--- a/wear/wear-complications-data/api/public_plus_experimental_current.txt
+++ b/wear/wear-complications-data/api/public_plus_experimental_current.txt
@@ -231,6 +231,18 @@
enum_constant public static final androidx.wear.complications.data.ComplicationType SMALL_IMAGE;
}
+ public final class CountDownTimeReference {
+ ctor public CountDownTimeReference(long dateTimeMillis);
+ method public long getDateTimeMillis();
+ property public final long dateTimeMillis;
+ }
+
+ public final class CountUpTimeReference {
+ ctor public CountUpTimeReference(long dateTimeMillis);
+ method public long getDateTimeMillis();
+ property public final long dateTimeMillis;
+ }
+
public final class DataKt {
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.wear.complications.data.ComplicationData asApiComplicationData(android.support.wearable.complications.ComplicationData);
}
@@ -481,7 +493,8 @@
}
public static final class TimeDifferenceComplicationText.Builder {
- ctor public TimeDifferenceComplicationText.Builder(androidx.wear.complications.data.TimeDifferenceStyle style, androidx.wear.complications.data.TimeReference reference);
+ ctor public TimeDifferenceComplicationText.Builder(androidx.wear.complications.data.TimeDifferenceStyle style, androidx.wear.complications.data.CountUpTimeReference countUpTimeReference);
+ ctor public TimeDifferenceComplicationText.Builder(androidx.wear.complications.data.TimeDifferenceStyle style, androidx.wear.complications.data.CountDownTimeReference countDownTimeReference);
method public androidx.wear.complications.data.TimeDifferenceComplicationText build();
method public androidx.wear.complications.data.TimeDifferenceComplicationText.Builder setDisplayAsNow(boolean displayAsNow);
method public androidx.wear.complications.data.TimeDifferenceComplicationText.Builder setMinimumUnit(java.util.concurrent.TimeUnit? minimumUnit);
@@ -532,25 +545,6 @@
method public androidx.wear.complications.data.TimeRange between(long startDateTimeMillis, long endDateTimeMillis);
}
- public final class TimeReference {
- method public static androidx.wear.complications.data.TimeReference between(long startDateTimeMillis, long endDateTimeMillis);
- method public static androidx.wear.complications.data.TimeReference ending(long dateTimeMillis);
- method public long getEndDateTimeMillis();
- method public long getStartDateTimeMillis();
- method public boolean hasEndDateTimeMillis();
- method public boolean hasStartDateTimeMillis();
- method public static androidx.wear.complications.data.TimeReference starting(long dateTimeMillis);
- property public final long endDateTimeMillis;
- property public final long startDateTimeMillis;
- field public static final androidx.wear.complications.data.TimeReference.Companion Companion;
- }
-
- public static final class TimeReference.Companion {
- method public androidx.wear.complications.data.TimeReference between(long startDateTimeMillis, long endDateTimeMillis);
- method public androidx.wear.complications.data.TimeReference ending(long dateTimeMillis);
- method public androidx.wear.complications.data.TimeReference starting(long dateTimeMillis);
- }
-
public final class TypeKt {
}
diff --git a/wear/wear-complications-data/api/restricted_current.txt b/wear/wear-complications-data/api/restricted_current.txt
index 77ff478..580e18d 100644
--- a/wear/wear-complications-data/api/restricted_current.txt
+++ b/wear/wear-complications-data/api/restricted_current.txt
@@ -289,6 +289,18 @@
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public int[] toWireTypes(java.util.Collection<? extends androidx.wear.complications.data.ComplicationType> types);
}
+ public final class CountDownTimeReference {
+ ctor public CountDownTimeReference(long dateTimeMillis);
+ method public long getDateTimeMillis();
+ property public final long dateTimeMillis;
+ }
+
+ public final class CountUpTimeReference {
+ ctor public CountUpTimeReference(long dateTimeMillis);
+ method public long getDateTimeMillis();
+ property public final long dateTimeMillis;
+ }
+
public final class DataKt {
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.wear.complications.data.ComplicationData asApiComplicationData(android.support.wearable.complications.ComplicationData);
}
@@ -539,7 +551,8 @@
}
public static final class TimeDifferenceComplicationText.Builder {
- ctor public TimeDifferenceComplicationText.Builder(androidx.wear.complications.data.TimeDifferenceStyle style, androidx.wear.complications.data.TimeReference reference);
+ ctor public TimeDifferenceComplicationText.Builder(androidx.wear.complications.data.TimeDifferenceStyle style, androidx.wear.complications.data.CountUpTimeReference countUpTimeReference);
+ ctor public TimeDifferenceComplicationText.Builder(androidx.wear.complications.data.TimeDifferenceStyle style, androidx.wear.complications.data.CountDownTimeReference countDownTimeReference);
method public androidx.wear.complications.data.TimeDifferenceComplicationText build();
method public androidx.wear.complications.data.TimeDifferenceComplicationText.Builder setDisplayAsNow(boolean displayAsNow);
method public androidx.wear.complications.data.TimeDifferenceComplicationText.Builder setMinimumUnit(java.util.concurrent.TimeUnit? minimumUnit);
@@ -590,25 +603,6 @@
method public androidx.wear.complications.data.TimeRange between(long startDateTimeMillis, long endDateTimeMillis);
}
- public final class TimeReference {
- method public static androidx.wear.complications.data.TimeReference between(long startDateTimeMillis, long endDateTimeMillis);
- method public static androidx.wear.complications.data.TimeReference ending(long dateTimeMillis);
- method public long getEndDateTimeMillis();
- method public long getStartDateTimeMillis();
- method public boolean hasEndDateTimeMillis();
- method public boolean hasStartDateTimeMillis();
- method public static androidx.wear.complications.data.TimeReference starting(long dateTimeMillis);
- property public final long endDateTimeMillis;
- property public final long startDateTimeMillis;
- field public static final androidx.wear.complications.data.TimeReference.Companion Companion;
- }
-
- public static final class TimeReference.Companion {
- method public androidx.wear.complications.data.TimeReference between(long startDateTimeMillis, long endDateTimeMillis);
- method public androidx.wear.complications.data.TimeReference ending(long dateTimeMillis);
- method public androidx.wear.complications.data.TimeReference starting(long dateTimeMillis);
- }
-
public final class TypeKt {
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.wear.complications.data.ComplicationType![] asApiComplicationTypes(int[]);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static int[] asWireTypes(java.util.Collection<? extends androidx.wear.complications.data.ComplicationType>);
diff --git a/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Data.kt b/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Data.kt
index 06b1f05..0d3548c 100644
--- a/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Data.kt
+++ b/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Data.kt
@@ -30,7 +30,8 @@
/** Base type for all different types of [ComplicationData] types. */
public sealed class ComplicationData constructor(
public val type: ComplicationType,
- public val tapAction: PendingIntent?
+ public val tapAction: PendingIntent?,
+ internal var cachedWireComplicationData: WireComplicationData?
) {
/**
* Converts this value to [WireComplicationData] object used for serialization.
@@ -49,6 +50,11 @@
* This must be checked for any time for which the complication will be displayed.
*/
public abstract fun isActiveAt(dateTimeMillis: Long): Boolean
+
+ internal fun createWireComplicationDataBuilder(): WireComplicationDataBuilder =
+ cachedWireComplicationData?.let {
+ WireComplicationDataBuilder(it)
+ } ?: WireComplicationDataBuilder(type.asWireComplicationType())
}
/** A pair of id and [ComplicationData]. */
@@ -71,7 +77,7 @@
* has no data to be displayed. Watch faces may choose whether to render this in some way or
* leave the slot empty.
*/
-public class NoDataComplicationData : ComplicationData(TYPE, null) {
+public class NoDataComplicationData : ComplicationData(TYPE, null, null) {
override fun isActiveAt(dateTimeMillis: Long): Boolean = true
/** @hide */
@@ -91,7 +97,7 @@
* i.e. when the user has chosen "Empty" in the provider chooser. Providers cannot send data of
* this type.
*/
-public class EmptyComplicationData : ComplicationData(TYPE, null) {
+public class EmptyComplicationData : ComplicationData(TYPE, null, null) {
override fun isActiveAt(dateTimeMillis: Long): Boolean = true
/** @hide */
@@ -112,7 +118,7 @@
* complication, and the watch face has not set a default provider. Providers cannot send data
* of this type.
*/
-public class NotConfiguredComplicationData : ComplicationData(TYPE, null) {
+public class NotConfiguredComplicationData : ComplicationData(TYPE, null, null) {
override fun isActiveAt(dateTimeMillis: Long): Boolean = true
/** @hide */
@@ -141,8 +147,9 @@
public val monochromaticImage: MonochromaticImage?,
public val contentDescription: ComplicationText?,
tapAction: PendingIntent?,
- public val validTimeRange: TimeRange?
-) : ComplicationData(TYPE, tapAction) {
+ public val validTimeRange: TimeRange?,
+ cachedWireComplicationData: WireComplicationData?
+) : ComplicationData(TYPE, tapAction, cachedWireComplicationData) {
public override fun isActiveAt(dateTimeMillis: Long): Boolean =
validTimeRange?.contains(dateTimeMillis) ?: true
@@ -158,6 +165,7 @@
private var title: ComplicationText? = null
private var monochromaticImage: MonochromaticImage? = null
private var contentDescription: ComplicationText? = null
+ private var cachedWireComplicationData: WireComplicationData? = null
/** Sets optional pending intent to be invoked when the complication is tapped. */
public fun setTapAction(tapAction: PendingIntent?): Builder = apply {
@@ -184,6 +192,12 @@
this.contentDescription = contentDescription
}
+ internal fun setCachedWireComplicationData(
+ cachedWireComplicationData: WireComplicationData?
+ ): Builder = apply {
+ this.cachedWireComplicationData = cachedWireComplicationData
+ }
+
/** Builds the [ShortTextComplicationData]. */
public fun build(): ShortTextComplicationData =
ShortTextComplicationData(
@@ -192,21 +206,22 @@
monochromaticImage,
contentDescription,
tapAction,
- validTimeRange
+ validTimeRange,
+ cachedWireComplicationData
)
}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
override fun asWireComplicationData(): WireComplicationData =
- WireComplicationDataBuilder(TYPE.asWireComplicationType()).apply {
+ createWireComplicationDataBuilder().apply {
setShortText(text.asWireComplicationText())
setShortTitle(title?.asWireComplicationText())
setContentDescription(contentDescription?.asWireComplicationText())
monochromaticImage?.addToWireComplicationData(this)
setTapAction(tapAction)
setValidTimeRange(validTimeRange, this)
- }.build()
+ }.build().also { cachedWireComplicationData = it }
/** @hide */
public companion object {
@@ -233,8 +248,9 @@
public val smallImage: SmallImage?,
public val contentDescription: ComplicationText?,
tapAction: PendingIntent?,
- public val validTimeRange: TimeRange?
-) : ComplicationData(TYPE, tapAction) {
+ public val validTimeRange: TimeRange?,
+ cachedWireComplicationData: WireComplicationData?
+) : ComplicationData(TYPE, tapAction, cachedWireComplicationData) {
public override fun isActiveAt(dateTimeMillis: Long): Boolean =
validTimeRange?.contains(dateTimeMillis) ?: true
@@ -251,6 +267,7 @@
private var monochromaticImage: MonochromaticImage? = null
private var smallImage: SmallImage? = null
private var contentDescription: ComplicationText? = null
+ private var cachedWireComplicationData: WireComplicationData? = null
/** Sets optional pending intent to be invoked when the complication is tapped. */
public fun setTapAction(tapAction: PendingIntent?): Builder = apply {
@@ -282,6 +299,12 @@
this.contentDescription = contentDescription
}
+ internal fun setCachedWireComplicationData(
+ cachedWireComplicationData: WireComplicationData?
+ ): Builder = apply {
+ this.cachedWireComplicationData = cachedWireComplicationData
+ }
+
/** Builds the [LongTextComplicationData]. */
public fun build(): LongTextComplicationData =
LongTextComplicationData(
@@ -291,14 +314,15 @@
smallImage,
contentDescription,
tapAction,
- validTimeRange
+ validTimeRange,
+ cachedWireComplicationData
)
}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
override fun asWireComplicationData(): WireComplicationData =
- WireComplicationDataBuilder(TYPE.asWireComplicationType()).apply {
+ createWireComplicationDataBuilder().apply {
setLongText(text.asWireComplicationText())
setLongTitle(title?.asWireComplicationText())
monochromaticImage?.addToWireComplicationData(this)
@@ -306,7 +330,7 @@
setTapAction(tapAction)
setContentDescription(contentDescription?.asWireComplicationText())
setValidTimeRange(validTimeRange, this)
- }.build()
+ }.build().also { cachedWireComplicationData = it }
/** @hide */
public companion object {
@@ -335,8 +359,9 @@
public val text: ComplicationText?,
public val contentDescription: ComplicationText?,
tapAction: PendingIntent?,
- public val validTimeRange: TimeRange?
-) : ComplicationData(TYPE, tapAction) {
+ public val validTimeRange: TimeRange?,
+ cachedWireComplicationData: WireComplicationData?
+) : ComplicationData(TYPE, tapAction, cachedWireComplicationData) {
public override fun isActiveAt(dateTimeMillis: Long): Boolean =
validTimeRange?.contains(dateTimeMillis) ?: true
@@ -357,6 +382,7 @@
private var title: ComplicationText? = null
private var text: ComplicationText? = null
private var contentDescription: ComplicationText? = null
+ private var cachedWireComplicationData: WireComplicationData? = null
/** Sets optional pending intent to be invoked when the complication is tapped. */
public fun setTapAction(tapAction: PendingIntent?): Builder = apply {
@@ -388,6 +414,12 @@
this.contentDescription = contentDescription
}
+ internal fun setCachedWireComplicationData(
+ cachedWireComplicationData: WireComplicationData?
+ ): Builder = apply {
+ this.cachedWireComplicationData = cachedWireComplicationData
+ }
+
/** Builds the [RangedValueComplicationData]. */
public fun build(): RangedValueComplicationData =
RangedValueComplicationData(
@@ -399,14 +431,15 @@
text,
contentDescription,
tapAction,
- validTimeRange
+ validTimeRange,
+ cachedWireComplicationData
)
}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
public override fun asWireComplicationData(): WireComplicationData =
- WireComplicationDataBuilder(TYPE.asWireComplicationType()).apply {
+ createWireComplicationDataBuilder().apply {
setRangedValue(value)
setRangedMinValue(min)
setRangedMaxValue(max)
@@ -416,7 +449,7 @@
setTapAction(tapAction)
setContentDescription(contentDescription?.asWireComplicationText())
setValidTimeRange(validTimeRange, this)
- }.build()
+ }.build().also { cachedWireComplicationData = it }
/** @hide */
public companion object {
@@ -440,8 +473,9 @@
public val monochromaticImage: MonochromaticImage,
public val contentDescription: ComplicationText?,
tapAction: PendingIntent?,
- public val validTimeRange: TimeRange?
-) : ComplicationData(TYPE, tapAction) {
+ public val validTimeRange: TimeRange?,
+ cachedWireComplicationData: WireComplicationData?
+) : ComplicationData(TYPE, tapAction, cachedWireComplicationData) {
public override fun isActiveAt(dateTimeMillis: Long): Boolean =
validTimeRange?.contains(dateTimeMillis) ?: true
@@ -455,6 +489,7 @@
private var tapAction: PendingIntent? = null
private var validTimeRange: TimeRange? = null
private var contentDescription: ComplicationText? = null
+ private var cachedWireComplicationData: WireComplicationData? = null
/** Sets optional pending intent to be invoked when the complication is tapped. */
public fun setTapAction(tapAction: PendingIntent?): Builder = apply {
@@ -471,24 +506,31 @@
this.contentDescription = contentDescription
}
+ internal fun setCachedWireComplicationData(
+ cachedWireComplicationData: WireComplicationData?
+ ): Builder = apply {
+ this.cachedWireComplicationData = cachedWireComplicationData
+ }
+
/** Builds the [MonochromaticImageComplicationData]. */
public fun build(): MonochromaticImageComplicationData =
MonochromaticImageComplicationData(
monochromaticImage,
contentDescription,
tapAction,
- validTimeRange
+ validTimeRange,
+ cachedWireComplicationData
)
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
override fun asWireComplicationData(): WireComplicationData =
- WireComplicationDataBuilder(TYPE.asWireComplicationType()).apply {
+ createWireComplicationDataBuilder().apply {
monochromaticImage.addToWireComplicationData(this)
setContentDescription(contentDescription?.asWireComplicationText())
setTapAction(tapAction)
setValidTimeRange(validTimeRange, this)
- }.build()
+ }.build().also { cachedWireComplicationData = it }
/** @hide */
public companion object {
@@ -512,8 +554,9 @@
public val smallImage: SmallImage,
public val contentDescription: ComplicationText?,
tapAction: PendingIntent?,
- public val validTimeRange: TimeRange?
-) : ComplicationData(TYPE, tapAction) {
+ public val validTimeRange: TimeRange?,
+ cachedWireComplicationData: WireComplicationData?
+) : ComplicationData(TYPE, tapAction, cachedWireComplicationData) {
public override fun isActiveAt(dateTimeMillis: Long): Boolean =
validTimeRange?.contains(dateTimeMillis) ?: true
@@ -527,6 +570,7 @@
private var tapAction: PendingIntent? = null
private var validTimeRange: TimeRange? = null
private var contentDescription: ComplicationText? = null
+ private var cachedWireComplicationData: WireComplicationData? = null
/** Sets optional pending intent to be invoked when the complication is tapped. */
public fun setTapAction(tapAction: PendingIntent?): Builder = apply {
@@ -543,19 +587,31 @@
this.contentDescription = contentDescription
}
+ internal fun setCachedWireComplicationData(
+ cachedWireComplicationData: WireComplicationData?
+ ): Builder = apply {
+ this.cachedWireComplicationData = cachedWireComplicationData
+ }
+
/** Builds the [MonochromaticImageComplicationData]. */
public fun build(): SmallImageComplicationData =
- SmallImageComplicationData(smallImage, contentDescription, tapAction, validTimeRange)
+ SmallImageComplicationData(
+ smallImage,
+ contentDescription,
+ tapAction,
+ validTimeRange,
+ cachedWireComplicationData
+ )
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
override fun asWireComplicationData(): WireComplicationData =
- WireComplicationDataBuilder(TYPE.asWireComplicationType()).apply {
+ createWireComplicationDataBuilder().apply {
smallImage.addToWireComplicationData(this)
setContentDescription(contentDescription?.asWireComplicationText())
setTapAction(tapAction)
setValidTimeRange(validTimeRange, this)
- }.build()
+ }.build().also { cachedWireComplicationData = it }
/** @hide */
public companion object {
@@ -583,8 +639,9 @@
public val photoImage: Icon,
public val contentDescription: ComplicationText?,
tapAction: PendingIntent?,
- public val validTimeRange: TimeRange?
-) : ComplicationData(TYPE, tapAction) {
+ public val validTimeRange: TimeRange?,
+ cachedWireComplicationData: WireComplicationData?
+) : ComplicationData(TYPE, tapAction, cachedWireComplicationData) {
public override fun isActiveAt(dateTimeMillis: Long): Boolean =
validTimeRange?.contains(dateTimeMillis) ?: true
@@ -598,6 +655,7 @@
private var tapAction: PendingIntent? = null
private var validTimeRange: TimeRange? = null
private var contentDescription: ComplicationText? = null
+ private var cachedWireComplicationData: WireComplicationData? = null
/** Sets optional pending intent to be invoked when the complication is tapped. */
@SuppressWarnings("MissingGetterMatchingBuilder") // See http://b/174052810
@@ -616,18 +674,30 @@
this.contentDescription = contentDescription
}
+ internal fun setCachedWireComplicationData(
+ cachedWireComplicationData: WireComplicationData?
+ ): Builder = apply {
+ this.cachedWireComplicationData = cachedWireComplicationData
+ }
+
/** Builds the [PhotoImageComplicationData]. */
public fun build(): PhotoImageComplicationData =
- PhotoImageComplicationData(photoImage, contentDescription, tapAction, validTimeRange)
+ PhotoImageComplicationData(
+ photoImage,
+ contentDescription,
+ tapAction,
+ validTimeRange,
+ cachedWireComplicationData
+ )
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
override fun asWireComplicationData(): WireComplicationData =
- WireComplicationDataBuilder(TYPE.asWireComplicationType()).apply {
+ createWireComplicationDataBuilder().apply {
setLargeImage(photoImage)
setContentDescription(contentDescription?.asWireComplicationText())
setValidTimeRange(validTimeRange, this)
- }.build()
+ }.build().also { cachedWireComplicationData = it }
/** @hide */
public companion object {
@@ -651,7 +721,8 @@
public val text: ComplicationText?,
public val title: ComplicationText?,
public val monochromaticImage: MonochromaticImage?,
-) : ComplicationData(TYPE, null) {
+ cachedWireComplicationData: WireComplicationData?
+) : ComplicationData(TYPE, null, cachedWireComplicationData) {
override fun isActiveAt(dateTimeMillis: Long): Boolean = true
@@ -664,6 +735,7 @@
private var text: ComplicationText? = null
private var title: ComplicationText? = null
private var monochromaticImage: MonochromaticImage? = null
+ private var cachedWireComplicationData: WireComplicationData? = null
/** Sets optional text associated with the complication data. */
public fun setText(text: ComplicationText?): Builder = apply {
@@ -680,19 +752,30 @@
this.monochromaticImage = monochromaticImage
}
+ internal fun setCachedWireComplicationData(
+ cachedWireComplicationData: WireComplicationData?
+ ): Builder = apply {
+ this.cachedWireComplicationData = cachedWireComplicationData
+ }
+
/** Builds the [NoPermissionComplicationData]. */
public fun build(): NoPermissionComplicationData =
- NoPermissionComplicationData(text, title, monochromaticImage)
+ NoPermissionComplicationData(
+ text,
+ title,
+ monochromaticImage,
+ cachedWireComplicationData
+ )
}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY)
override fun asWireComplicationData(): WireComplicationData =
- WireComplicationDataBuilder(TYPE.asWireComplicationType()).apply {
+ createWireComplicationDataBuilder().apply {
setShortText(text?.asWireComplicationText())
setShortTitle(title?.asWireComplicationText())
monochromaticImage?.addToWireComplicationData(this)
- }.build()
+ }.build().also { cachedWireComplicationData = it }
/** @hide */
public companion object {
@@ -703,8 +786,9 @@
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public fun WireComplicationData.asApiComplicationData(): ComplicationData =
- when (type) {
+public fun WireComplicationData.asApiComplicationData(): ComplicationData {
+ val wireComplicationData = this
+ return when (type) {
NoDataComplicationData.TYPE.asWireComplicationType() -> NoDataComplicationData()
EmptyComplicationData.TYPE.asWireComplicationType() -> EmptyComplicationData()
@@ -719,6 +803,7 @@
setTitle(shortTitle?.asApiComplicationText())
setMonochromaticImage(parseIcon())
setContentDescription(contentDescription?.asApiComplicationText())
+ setCachedWireComplicationData(wireComplicationData)
}.build()
LongTextComplicationData.TYPE.asWireComplicationType() ->
@@ -729,6 +814,7 @@
setMonochromaticImage(parseIcon())
setSmallImage(parseSmallImage())
setContentDescription(contentDescription?.asApiComplicationText())
+ setCachedWireComplicationData(wireComplicationData)
}.build()
RangedValueComplicationData.TYPE.asWireComplicationType() ->
@@ -742,6 +828,7 @@
setTitle(shortTitle?.asApiComplicationText())
setText(shortText?.asApiComplicationText())
setContentDescription(contentDescription?.asApiComplicationText())
+ setCachedWireComplicationData(wireComplicationData)
}.build()
MonochromaticImageComplicationData.TYPE.asWireComplicationType() ->
@@ -749,6 +836,7 @@
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
setContentDescription(contentDescription?.asApiComplicationText())
+ setCachedWireComplicationData(wireComplicationData)
}.build()
SmallImageComplicationData.TYPE.asWireComplicationType() ->
@@ -756,12 +844,14 @@
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
setContentDescription(contentDescription?.asApiComplicationText())
+ setCachedWireComplicationData(wireComplicationData)
}.build()
PhotoImageComplicationData.TYPE.asWireComplicationType() ->
PhotoImageComplicationData.Builder(largeImage!!).apply {
setValidTimeRange(parseTimeRange())
setContentDescription(contentDescription?.asApiComplicationText())
+ setCachedWireComplicationData(wireComplicationData)
}.build()
NoPermissionComplicationData.TYPE.asWireComplicationType() ->
@@ -769,10 +859,12 @@
setMonochromaticImage(parseIcon())
setTitle(shortTitle?.asApiComplicationText())
setText(shortText?.asApiComplicationText())
+ setCachedWireComplicationData(wireComplicationData)
}.build()
else -> NoDataComplicationData()
}
+}
private fun WireComplicationData.parseTimeRange() =
if ((startDateTimeMillis == 0L) and (endDateTimeMillis == Long.MAX_VALUE)) {
@@ -813,4 +905,4 @@
data.setEndDateTimeMillis(it.endDateTimeMillis)
}
}
-}
\ No newline at end of file
+}
diff --git a/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Text.kt b/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Text.kt
index ea85f74..2d5b55c 100644
--- a/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Text.kt
+++ b/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Text.kt
@@ -191,14 +191,25 @@
*
* Requires setting a [TimeDifferenceStyle].
*/
- public class Builder(
+ public class Builder private constructor(
private val style: TimeDifferenceStyle,
- private val reference: TimeReference
+ private val startDateTimeMillis: Long?,
+ private val endDateTimeMillis: Long?
) {
private var text: CharSequence? = null
private var displayAsNow: Boolean? = null
private var minimumUnit: TimeUnit? = null
+ public constructor(
+ style: TimeDifferenceStyle,
+ countUpTimeReference: CountUpTimeReference
+ ) : this(style, countUpTimeReference.dateTimeMillis, null)
+
+ public constructor(
+ style: TimeDifferenceStyle,
+ countDownTimeReference: CountDownTimeReference
+ ) : this(style, null, countDownTimeReference.dateTimeMillis)
+
/**
* Sets the text within which the time difference will be displayed.
*
@@ -247,11 +258,11 @@
WireComplicationTextTimeDifferenceBuilder().apply {
setStyle(style.wireStyle)
setSurroundingText(text)
- if (reference.hasStartDateTimeMillis()) {
- setReferencePeriodStartMillis(reference.startDateTimeMillis)
+ startDateTimeMillis?.let {
+ setReferencePeriodStartMillis(it)
}
- if (reference.hasEndDateTimeMillis()) {
- setReferencePeriodEndMillis(reference.endDateTimeMillis)
+ endDateTimeMillis?.let {
+ setReferencePeriodEndMillis(it)
}
displayAsNow?.let { setShowNowText(it) }
setMinimumUnit(minimumUnit)
diff --git a/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Time.kt b/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Time.kt
index f4f99e7..1205662 100644
--- a/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Time.kt
+++ b/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Time.kt
@@ -48,40 +48,8 @@
}
}
-/**
- * Expresses a reference point or range for a time difference.
- *
- * It defines [endDateTimeMillis] and/or [startDateTimeMillis] to express the corresponding
- * time differences relative to before, between or after the given point(s) in time.
- */
-public class TimeReference internal constructor(
- public val endDateTimeMillis: Long,
- public val startDateTimeMillis: Long
-) {
- public fun hasStartDateTimeMillis(): Boolean = startDateTimeMillis != NONE
- public fun hasEndDateTimeMillis(): Boolean = endDateTimeMillis != NONE
+/** Defines a point in time the complication is counting down until. */
+public class CountDownTimeReference(public val dateTimeMillis: Long)
- public companion object {
- private const val NONE = -1L
-
- /**
- * Creates a [TimeReference] for the time difference ending at the given [dateTimeMillis].
- */
- @JvmStatic
- public fun ending(dateTimeMillis: Long): TimeReference = TimeReference(dateTimeMillis, NONE)
-
- /**
- * Creates a [TimeReference] for the time difference starting at the given [dateTimeMillis].
- */
- @JvmStatic
- public fun starting(dateTimeMillis: Long): TimeReference =
- TimeReference(NONE, dateTimeMillis)
-
- /**
- * Creates a [TimeReference] for the time difference between [startDateTimeMillis] and [endDateTimeMillis].
- */
- @JvmStatic
- public fun between(startDateTimeMillis: Long, endDateTimeMillis: Long): TimeReference =
- TimeReference(endDateTimeMillis, startDateTimeMillis)
- }
-}
+/** Defines a point in time the complication is counting up from. */
+public class CountUpTimeReference(public val dateTimeMillis: Long)
diff --git a/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/DataTest.kt b/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/DataTest.kt
index 42c5ec4..9d93102 100644
--- a/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/DataTest.kt
+++ b/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/DataTest.kt
@@ -35,6 +35,7 @@
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA).build()
)
+ testRoundTripConversions(data)
}
@Test
@@ -44,6 +45,7 @@
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_EMPTY).build()
)
+ testRoundTripConversions(data)
}
@Test
@@ -53,6 +55,7 @@
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_NOT_CONFIGURED).build()
)
+ testRoundTripConversions(data)
}
@Test
@@ -69,6 +72,7 @@
.setContentDescription(WireComplicationText.plainText("content description"))
.build()
)
+ testRoundTripConversions(data)
}
@Test
@@ -85,6 +89,7 @@
.setContentDescription(WireComplicationText.plainText("content description"))
.build()
)
+ testRoundTripConversions(data)
}
@Test
@@ -103,6 +108,7 @@
.setContentDescription(WireComplicationText.plainText("content description"))
.build()
)
+ testRoundTripConversions(data)
}
@Test
@@ -118,6 +124,7 @@
.setContentDescription(WireComplicationText.plainText("content description"))
.build()
)
+ testRoundTripConversions(data)
}
@Test
@@ -134,6 +141,7 @@
.setContentDescription(WireComplicationText.plainText("content description"))
.build()
)
+ testRoundTripConversions(data)
}
@Test
@@ -149,6 +157,7 @@
.setContentDescription(WireComplicationText.plainText("content description"))
.build()
)
+ testRoundTripConversions(data)
}
@Test
@@ -162,6 +171,14 @@
.setShortText(WireComplicationText.plainText("needs location"))
.build()
)
+ testRoundTripConversions(data)
+ }
+
+ private fun testRoundTripConversions(data: ComplicationData) {
+ ParcelableSubject.assertThat(data.asWireComplicationData())
+ .hasSameSerializationAs(
+ data.asWireComplicationData().asApiComplicationData().asWireComplicationData()
+ )
}
}
diff --git a/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TextTest.kt b/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TextTest.kt
index 6798909..127676d 100644
--- a/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TextTest.kt
+++ b/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TextTest.kt
@@ -45,10 +45,40 @@
}
@Test
- public fun timeDifferenceText() {
+ public fun timeDifferenceText_CountUpTimeReference() {
+ val referenceMillis = Instant.parse("2020-12-30T10:15:30.001Z").toEpochMilli()
val text = TimeDifferenceComplicationText.Builder(
TimeDifferenceStyle.STOPWATCH,
- TimeReference.starting(10000L)
+ CountDownTimeReference(referenceMillis)
+ )
+ .setText("^1 after lunch")
+ .setDisplayAsNow(false)
+ .setMinimumUnit(TimeUnit.SECONDS)
+ .build()
+
+ ParcelableSubject.assertThat(text.asWireComplicationText())
+ .hasSameSerializationAs(
+ WireTimeDifferenceBuilder()
+ .setStyle(WireComplicationText.DIFFERENCE_STYLE_STOPWATCH)
+ .setSurroundingText("^1 after lunch")
+ .setShowNowText(false)
+ .setMinimumUnit(TimeUnit.SECONDS)
+ .setReferencePeriodEndMillis(referenceMillis)
+ .build()
+ )
+
+ val twoMinutesThreeSecondAfter = referenceMillis + 2.minutes + 3.seconds
+ assertThat(
+ text.getTextAt(getResource(), twoMinutesThreeSecondAfter).toString()
+ ).isEqualTo("02:03 after lunch")
+ }
+
+ @Test
+ public fun timeDifferenceText_CountDownTimeReference() {
+ val referenceMillis = Instant.parse("2020-12-30T10:15:30.001Z").toEpochMilli()
+ val text = TimeDifferenceComplicationText.Builder(
+ TimeDifferenceStyle.STOPWATCH,
+ CountUpTimeReference(referenceMillis)
)
.setText("^1 before lunch")
.setDisplayAsNow(false)
@@ -62,9 +92,14 @@
.setSurroundingText("^1 before lunch")
.setShowNowText(false)
.setMinimumUnit(TimeUnit.SECONDS)
- .setReferencePeriodStartMillis(10000L)
+ .setReferencePeriodStartMillis(referenceMillis)
.build()
)
+
+ val twoMinutesThreeSecondBefore = referenceMillis - 2.minutes - 3.seconds
+ assertThat(
+ text.getTextAt(getResource(), twoMinutesThreeSecondBefore).toString()
+ ).isEqualTo("02:03 before lunch")
}
@Test
@@ -85,6 +120,8 @@
.build()
)
}
+
+ private fun getResource() = ApplicationProvider.getApplicationContext<Context>().resources
}
@RunWith(SharedRobolectricTestRunner::class)
diff --git a/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TimeTest.kt b/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TimeRangeTest.kt
similarity index 79%
rename from wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TimeTest.kt
rename to wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TimeRangeTest.kt
index a81ba14..4c002ef 100644
--- a/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TimeTest.kt
+++ b/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TimeRangeTest.kt
@@ -70,23 +70,4 @@
assertThat(range.contains(10000)).isTrue()
assertThat(range.contains(Long.MAX_VALUE)).isTrue()
}
-}
-
-@RunWith(SharedRobolectricTestRunner::class)
-public class TimeReferenceTest {
- @Test
- public fun startingAtTime() {
- val reference = TimeReference.starting(1000L)
- assertThat(reference.hasStartDateTimeMillis()).isTrue()
- assertThat(reference.startDateTimeMillis).isEqualTo(1000L)
- assertThat(reference.hasEndDateTimeMillis()).isFalse()
- }
-
- @Test
- public fun endingAtTime() {
- val reference = TimeReference.ending(1000L)
- assertThat(reference.hasStartDateTimeMillis()).isFalse()
- assertThat(reference.hasEndDateTimeMillis()).isTrue()
- assertThat(reference.endDateTimeMillis).isEqualTo(1000L)
- }
-}
+}
\ No newline at end of file
diff --git a/wear/wear-tiles-data/build.gradle b/wear/wear-tiles-data/build.gradle
deleted file mode 100644
index faf2576..0000000
--- a/wear/wear-tiles-data/build.gradle
+++ /dev/null
@@ -1,63 +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.
- */
-
-import static androidx.build.dependencies.DependenciesKt.*
-import androidx.build.LibraryGroups
-import androidx.build.LibraryType
-import androidx.build.LibraryVersions
-import androidx.build.RunApiTasks
-
-plugins {
- id("AndroidXPlugin")
- id("com.android.library")
-}
-
-dependencies {
- api("androidx.annotation:annotation:1.1.0")
- api(GUAVA_LISTENABLE_FUTURE)
- implementation(PROTOBUF_LITE)
- implementation("androidx.annotation:annotation:1.2.0-alpha01")
-
- testImplementation(ANDROIDX_TEST_EXT_JUNIT)
- testImplementation(ANDROIDX_TEST_CORE)
- testImplementation(ANDROIDX_TEST_RUNNER)
- testImplementation(ANDROIDX_TEST_RULES)
- testImplementation(ROBOLECTRIC)
- testImplementation(MOCKITO_CORE)
-}
-
-android {
- defaultConfig {
- minSdkVersion 25
- }
-
- buildFeatures {
- aidl = true
- }
-
- // Use Robolectric 4.+
- testOptions.unitTests.includeAndroidResources = true
-}
-
-androidx {
- name = "Android Wear Tiles Data"
- type = LibraryType.PUBLISHED_LIBRARY
- mavenGroup = LibraryGroups.WEAR
- mavenVersion = LibraryVersions.WEAR_TILES_DATA
- inceptionYear = "2020"
- description = "Android Wear Tiles Internal Data Classes"
- runApiTasks = new RunApiTasks.No("API tracking disabled while the package is empty")
-}
diff --git a/wear/wear-tiles-data/lint-baseline.xml b/wear/wear-tiles-data/lint-baseline.xml
deleted file mode 100644
index 8f1aa4b..0000000
--- a/wear/wear-tiles-data/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="5" by="lint 4.2.0-beta02" client="gradle" variant="debug" version="4.2.0-beta02">
-
-</issues>
diff --git a/wear/wear-tiles-data/src/main/AndroidManifest.xml b/wear/wear-tiles-data/src/main/AndroidManifest.xml
deleted file mode 100644
index e26eded..0000000
--- a/wear/wear-tiles-data/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="androidx.wear.tiles">
-</manifest>
diff --git a/wear/wear-tiles-renderer/build.gradle b/wear/wear-tiles-renderer/build.gradle
index 70bdc56..b405d45 100644
--- a/wear/wear-tiles-renderer/build.gradle
+++ b/wear/wear-tiles-renderer/build.gradle
@@ -26,6 +26,7 @@
id("AndroidXPlugin")
id("com.android.library")
id("kotlin-android")
+ id("com.google.protobuf")
}
dependencies {
@@ -39,6 +40,14 @@
implementation(KOTLIN_COROUTINES_CORE)
implementation(KOTLIN_COROUTINES_ANDROID)
+ androidTestImplementation(project(path: ":wear:wear-tiles-proto", configuration: "shadow"))
+ androidTestImplementation(project(":test-screenshot"))
+ androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
+ androidTestImplementation(ANDROIDX_TEST_CORE)
+ androidTestImplementation(ANDROIDX_TEST_RUNNER)
+ androidTestImplementation(ANDROIDX_TEST_RULES)
+ androidTestImplementation("com.google.protobuf:protobuf-java:3.10.0")
+
testImplementation(ANDROIDX_TEST_EXT_JUNIT)
testImplementation(ANDROIDX_TEST_EXT_TRUTH)
testImplementation(ANDROIDX_TEST_CORE)
@@ -52,11 +61,15 @@
android {
defaultConfig {
- minSdkVersion 25
+ minSdkVersion 26
}
// Use Robolectric 4.+
testOptions.unitTests.includeAndroidResources = true
+
+ sourceSets {
+ androidTest.assets.srcDirs += project.rootDir.absolutePath + "/../../golden/wear/wear-tiles-renderer"
+ }
}
// Allow usage of Kotlin's @OptIn.
@@ -66,6 +79,23 @@
}
}
+protobuf {
+ protoc {
+ artifact = "com.google.protobuf:protoc:3.10.0"
+ }
+
+ // Generates the java proto-lite code for the protos in this project. See
+ // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
+ // for more information.
+ generateProtoTasks {
+ all().each { task ->
+ task.builtins {
+ java {}
+ }
+ }
+ }
+}
+
androidx {
name = "Android Wear Tiles Renderer"
type = LibraryType.PUBLISHED_LIBRARY
diff --git a/wear/wear-tiles-renderer/src/androidTest/AndroidManifest.xml b/wear/wear-tiles-renderer/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..6c1bac5
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.wear.tiles.renderer.test">
+</manifest>
diff --git a/wear/wear-tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java b/wear/wear-tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java
new file mode 100644
index 0000000..ad0b0b7
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.tiles.renderer.test;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.view.Gravity;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.widget.FrameLayout;
+
+import androidx.core.content.ContextCompat;
+import androidx.test.filters.LargeTest;
+import androidx.test.screenshot.AndroidXScreenshotTestRule;
+import androidx.test.screenshot.matchers.MSSIMMatcher;
+import androidx.wear.tiles.builders.LayoutElementBuilders;
+import androidx.wear.tiles.builders.ResourceBuilders;
+import androidx.wear.tiles.proto.LayoutElementProto.Layout;
+import androidx.wear.tiles.proto.LayoutElementProto.LayoutElement;
+import androidx.wear.tiles.proto.ResourceProto.AndroidImageResourceByResId;
+import androidx.wear.tiles.proto.ResourceProto.ImageFormat;
+import androidx.wear.tiles.proto.ResourceProto.ImageResource;
+import androidx.wear.tiles.proto.ResourceProto.InlineImageResource;
+import androidx.wear.tiles.proto.ResourceProto.Resources;
+import androidx.wear.tiles.protobuf.ByteString;
+import androidx.wear.tiles.renderer.StandardResourceAccessors;
+import androidx.wear.tiles.renderer.TileRenderer;
+
+import com.google.protobuf.TextFormat;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Arrays;
+import java.util.Collection;
+
+@RunWith(Parameterized.class)
+@LargeTest
+public class TileRendererGoldenTest {
+ @Parameterized.Parameters(name = "{0}")
+ public static Collection<Object[]> data() {
+ return Arrays.asList(
+ new Object[][] {
+ {"all_modifiers"},
+ {"arc_above_360"},
+ {"arc_alignment_mixed_types"},
+ {"arc_alignment"},
+ {"arc_anchors"},
+ {"arc_text_and_lines"},
+ {"arc_with_buttons_rotated"},
+ {"arc_with_buttons_unrotated"},
+ {"box_with_corners_and_border_rtlaware"},
+ {"box_with_corners_and_border"},
+ {"box_with_fixed_size"},
+ {"broken_drawable"},
+ {"column_with_alignment_rtlaware"},
+ {"column_with_alignment"},
+ {"column_with_height"},
+ {"expanded_box_horizontal_right_align"},
+ {"expanded_box_horizontal"},
+ {"expanded_box_vertical"},
+ {"expanded_children_in_row"},
+ {"font_weights_in_arc"},
+ {"font_weights_in_spannable"},
+ {"image_expanded_to_parent"},
+ {"image_expand_modes"},
+ {"image_oversized_in_box_proportional"},
+ {"image_oversized_in_box"},
+ {"image_proportional_resize"},
+ {"image_with_dimensions"},
+ {"image_with_inline_data"},
+ {"image_with_padding"},
+ {"line_in_arc"},
+ {"line_multi_height"},
+ {"long_text"},
+ {"multi_line_text_alignment"},
+ {"row_column_space_test"},
+ {"row_with_alignment"},
+ {"row_with_width"},
+ {"simple_text"},
+ {"single_line_text_alignment"},
+ {"spacer_horizontal"},
+ {"spacer_in_arc"},
+ {"spacer_vertical"},
+ {"spannable_image"},
+ {"spannable_image_with_clickable"},
+ {"spannable_image_wrapped"},
+ {"spannable_text"},
+ {"text_and_image_in_box"},
+ {"text_default_size"},
+ {"text_in_column"},
+ {"text_in_row"},
+ {"text_with_font_weights_italic"},
+ {"text_with_font_weights"},
+ {"text_with_spacing"},
+ });
+ }
+
+ @Rule
+ public AndroidXScreenshotTestRule screenshotRule =
+ new AndroidXScreenshotTestRule("wear/wear-tiles-renderer");
+
+ // This isn't totally ideal right now.
+ // The screenshot tests run on a phone, so emulate some watch dimensions here.
+ private static final int SCREEN_WIDTH = 390;
+ private static final int SCREEN_HEIGHT = 390;
+
+ private static final int INLINE_IMAGE_WIDTH = 8;
+ private static final int INLINE_IMAGE_HEIGHT = 8;
+ private static final int INLINE_IMAGE_PIXEL_STRIDE = 2; // RGB565 = 2 bytes per pixel
+
+ private final String mProtoFile;
+
+ public TileRendererGoldenTest(String protoFile) {
+ mProtoFile = protoFile;
+ }
+
+ @Test
+ public void renderer_goldenTest() throws Exception {
+ int id =
+ getApplicationContext()
+ .getResources()
+ .getIdentifier(mProtoFile, "raw", getApplicationContext().getPackageName());
+
+ runSingleScreenshotTest(id, mProtoFile);
+ }
+
+ private static Resources generateResources() {
+ byte[] inlineImagePayload =
+ new byte[INLINE_IMAGE_WIDTH * INLINE_IMAGE_HEIGHT * INLINE_IMAGE_PIXEL_STRIDE];
+
+ // Generate a square image, with a white square in the center.
+ // This replaces an inline payload as a byte array. We could hardcode it, but the
+ // autoformatter will ruin the formatting.
+ for (int y = 0; y < 8; y++) {
+ for (int x = 0; x < 8; x++) {
+ int index = ((y * INLINE_IMAGE_WIDTH) + x) * INLINE_IMAGE_PIXEL_STRIDE;
+ short color = 0x0000;
+
+ if (y > 2 && y < 6 && x > 2 && x < 6) {
+ color = (short) 0xFFFF;
+ }
+
+ inlineImagePayload[index + 0] = (byte) ((color >> 0) & 0xFF);
+ inlineImagePayload[index + 1] = (byte) ((color >> 8) & 0xFF);
+ }
+ }
+
+ return Resources.newBuilder()
+ .putIdToImage(
+ "android",
+ ImageResource.newBuilder()
+ .setAndroidResourceByResid(
+ AndroidImageResourceByResId.newBuilder()
+ .setResourceId(R.drawable.android_24dp))
+ .build())
+ .putIdToImage(
+ "android_withbg_120dp",
+ ImageResource.newBuilder()
+ .setAndroidResourceByResid(
+ AndroidImageResourceByResId.newBuilder()
+ .setResourceId(R.mipmap.android_withbg_120dp))
+ .build())
+ .putIdToImage(
+ "inline",
+ ImageResource.newBuilder()
+ .setInlineResource(
+ InlineImageResource.newBuilder()
+ .setFormat(ImageFormat.IMAGE_FORMAT_RGB_565)
+ .setWidthPx(INLINE_IMAGE_WIDTH)
+ .setHeightPx(INLINE_IMAGE_HEIGHT)
+ .setData(ByteString.copyFrom(inlineImagePayload)))
+ .build())
+ .putIdToImage(
+ "broken_image",
+ ImageResource.newBuilder()
+ .setAndroidResourceByResid(
+ AndroidImageResourceByResId.newBuilder()
+ .setResourceId(R.drawable.broken_drawable))
+ .build())
+ .putIdToImage(
+ "missing_image",
+ ImageResource.newBuilder()
+ .setAndroidResourceByResid(
+ AndroidImageResourceByResId.newBuilder().setResourceId(-1))
+ .build())
+ .build();
+ }
+
+ private void runSingleScreenshotTest(int protoResId, String expectedKey) throws Exception {
+ FrameLayout mainFrame = new FrameLayout(getApplicationContext());
+ mainFrame.setTextAlignment(View.TEXT_ALIGNMENT_TEXT_START);
+
+ Context appContext = getApplicationContext();
+
+ // This is a hack, but use the full proto lib to translate the textproto into a serialized
+ // proto, then pass into a Layout.
+ TextFormat.Parser parser = TextFormat.getParser();
+ androidx.wear.tiles.testing.proto.LayoutElementProto.LayoutElement.Builder
+ layoutElementProto =
+ androidx.wear.tiles.testing.proto.LayoutElementProto.LayoutElement
+ .newBuilder();
+
+ InputStream rawResStream =
+ getApplicationContext().getResources().openRawResource(protoResId);
+ try (InputStreamReader reader = new InputStreamReader(rawResStream)) {
+ parser.merge(reader, layoutElementProto);
+ }
+
+ byte[] contents = layoutElementProto.build().toByteArray();
+
+ // Inflate and go!
+ LayoutElement rootElement = LayoutElement.parseFrom(contents);
+
+ TileRenderer renderer =
+ new TileRenderer(
+ appContext,
+ LayoutElementBuilders.Layout.fromProto(
+ Layout.newBuilder().setRoot(rootElement).build()),
+ StandardResourceAccessors.forLocalApp(
+ appContext,
+ ResourceBuilders.Resources.fromProto(generateResources()))
+ .build(),
+ ContextCompat.getMainExecutor(getApplicationContext()),
+ i -> {});
+
+ View firstChild = renderer.inflate(mainFrame);
+
+ if (firstChild == null) {
+ throw new RuntimeException("Failed to inflate " + expectedKey);
+ }
+
+ // Simulate what the thing outside the renderer should do. Fix the frame at the "screen"
+ // size, and center the contents.
+ FrameLayout.LayoutParams layoutParams =
+ (FrameLayout.LayoutParams) firstChild.getLayoutParams();
+ layoutParams.gravity = Gravity.CENTER;
+
+ int screenWidth = MeasureSpec.makeMeasureSpec(SCREEN_WIDTH, MeasureSpec.EXACTLY);
+ int screenHeight = MeasureSpec.makeMeasureSpec(SCREEN_HEIGHT, MeasureSpec.EXACTLY);
+
+ mainFrame.measure(screenWidth, screenHeight);
+ mainFrame.layout(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
+
+ // Blit it to a bitmap for further testing.
+ Bitmap bmp = Bitmap.createBitmap(SCREEN_WIDTH, SCREEN_HEIGHT, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bmp);
+ mainFrame.draw(canvas);
+
+ screenshotRule.assertBitmapAgainstGolden(bmp, expectedKey, new MSSIMMatcher());
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/proto/action.proto b/wear/wear-tiles-renderer/src/androidTest/proto/action.proto
new file mode 100644
index 0000000..eda1f13
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/proto/action.proto
@@ -0,0 +1,42 @@
+// Actions that can be performed when a user interacts with layout elements.
+syntax = "proto3";
+
+package androidx.wear.tiles.testing.proto;
+
+import "state.proto";
+
+option java_package = "androidx.wear.tiles.testing.proto";
+option java_outer_classname = "ActionProto";
+
+// A launch action to send an intent to an Android activity.
+message AndroidActivity {
+ // The package name to send the intent to, for example, "com.google.weather".
+ string package_name = 1;
+
+ // The fully qualified class name (including the package) to send the intent
+ // to, for example, "com.google.weather.WeatherOverviewActivity".
+ string class_name = 2;
+}
+
+// An action used to launch another activity on the system. This can hold
+// multiple different underlying action types, which will be picked based on
+// what the underlying runtime believes to be suitable.
+message LaunchAction {
+ // An action to launch an Android activity.
+ AndroidActivity android_activity = 1;
+}
+
+// An action used to load (or reload) the tile contents.
+message LoadAction {
+ // The state to load the next tile with. This will be included in the
+ // TileRequest sent after this action is invoked by a Clickable.
+ State request_state = 1;
+}
+
+// An action that can be used by a layout element.
+message Action {
+ oneof value {
+ LaunchAction launch_action = 1;
+ LoadAction load_action = 2;
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/proto/color.proto b/wear/wear-tiles-renderer/src/androidTest/proto/color.proto
new file mode 100644
index 0000000..68cde643
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/proto/color.proto
@@ -0,0 +1,13 @@
+// Color utilities for layout elements.
+syntax = "proto3";
+
+package androidx.wear.tiles.testing.proto;
+
+option java_package = "androidx.wear.tiles.testing.proto";
+option java_outer_classname = "ColorProto";
+
+// A property defining a color.
+message ColorProp {
+ // The color value, in ARGB format.
+ uint32 argb = 1;
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/proto/device_parameters.proto b/wear/wear-tiles-renderer/src/androidTest/proto/device_parameters.proto
new file mode 100644
index 0000000..f7b34ff
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/proto/device_parameters.proto
@@ -0,0 +1,49 @@
+// Request messages used to fetch tiles and resources
+syntax = "proto3";
+
+package androidx.wear.tiles.testing.proto;
+
+option java_package = "androidx.wear.tiles.testing.proto";
+option java_outer_classname = "DeviceParametersProto";
+
+// The platform of the device requesting a tile.
+enum DevicePlatform {
+ // Device platform is undefined.
+ DEVICE_PLATFORM_UNDEFINED = 0;
+
+ // Device is a Wear OS by Google device.
+ DEVICE_PLATFORM_WEAR_OS = 1;
+}
+
+// The shape of a screen.
+enum ScreenShape {
+ // Screen shape is undefined.
+ SCREEN_SHAPE_UNDEFINED = 0;
+
+ // A round screen (typically found on most Wear devices).
+ SCREEN_SHAPE_ROUND = 1;
+
+ // Rectangular screens.
+ SCREEN_SHAPE_RECT = 2;
+}
+
+// Parameters describing the device requesting a tile update. This contains
+// physical and logical characteristics about the device (e.g. screen size and
+// density, etc).
+message DeviceParameters {
+ // Width of the device's screen in DP.
+ uint32 screen_width_dp = 1;
+
+ // Height of the device's screen in DP.
+ uint32 screen_height_dp = 2;
+
+ // Density of the display. This value is the scaling factor to get from DP to
+ // Pixels (px = dp * density).
+ float screen_density = 3;
+
+ // The platform of the device.
+ DevicePlatform device_platform = 4;
+
+ // The shape of the device's screen
+ ScreenShape screen_shape = 5;
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/proto/dimension.proto b/wear/wear-tiles-renderer/src/androidTest/proto/dimension.proto
new file mode 100644
index 0000000..e53ddea
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/proto/dimension.proto
@@ -0,0 +1,85 @@
+// Dimensions for layout elements.
+syntax = "proto3";
+
+package androidx.wear.tiles.testing.proto;
+
+
+option java_package = "androidx.wear.tiles.testing.proto";
+option java_outer_classname = "DimensionProto";
+
+// A type for linear dimensions, measured in dp.
+message DpProp {
+ // The value, in dp.
+ float value = 1;
+}
+
+// A type for font sizes, measured in sp.
+message SpProp {
+ // The value, in sp.
+ float value = 2;
+
+ reserved 1;
+}
+
+// A type for font spacing, measured in em.
+message EmProp {
+ // The value, in em.
+ float value = 1;
+}
+
+// A type for angular dimensions, measured in degrees.
+message DegreesProp {
+ // The value, in degrees.
+ float value = 1;
+}
+
+// A type for a dimension that fills all the space it can (i.e. MATCH_PARENT in
+// Android parlance)
+message ExpandedDimensionProp {}
+
+// A type for a dimension that sizes itself to the size of its children (i.e.
+// WRAP_CONTENT in Android parlance)
+message WrappedDimensionProp {}
+
+// A type for a dimension that scales itself proportionally to another dimension
+// such that the aspect ratio defined by the given width and height values is
+// preserved.
+//
+// Note that the width and height are unitless; only their ratio is relevant.
+// This allows for specifying an element's size using common ratios (e.g.
+// width=4, height=3), or to allow an element to be resized proportionally based
+// on the size of an underlying asset (e.g. an 800x600 image being added to a
+// smaller container and resized accordingly).
+message ProportionalDimensionProp {
+ // The width to be used when calculating the aspect ratio to preserve.
+ uint32 aspect_ratio_width = 1;
+
+ // The height to be used when calculating the aspect ratio ratio to preserve.
+ uint32 aspect_ratio_height = 2;
+}
+
+// A dimension that can be applied to a container.
+message ContainerDimension {
+ oneof inner {
+ DpProp linear_dimension = 1;
+ ExpandedDimensionProp expanded_dimension = 2;
+ WrappedDimensionProp wrapped_dimension = 3;
+ }
+}
+
+// A dimension that can be applied to an image.
+message ImageDimension {
+ oneof inner {
+ DpProp linear_dimension = 1;
+ ExpandedDimensionProp expanded_dimension = 2;
+ ProportionalDimensionProp proportional_dimension = 3;
+ }
+}
+
+// A dimension that can be applied to a spacer.
+message SpacerDimension {
+ oneof inner {
+ DpProp linear_dimension = 1;
+ // TODO(b/169137847): Add ExpandedDimensionProp
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/proto/events.proto b/wear/wear-tiles-renderer/src/androidTest/proto/events.proto
new file mode 100644
index 0000000..350394c
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/proto/events.proto
@@ -0,0 +1,34 @@
+// Messages used when events happen in the Tiles system.
+syntax = "proto3";
+
+package androidx.wear.tiles.testing.proto;
+
+
+option java_package = "androidx.wear.tiles.testing.proto";
+option java_outer_classname = "EventProto";
+
+// Event fired when a tile has been added to the carousel.
+message TileAddEvent {
+ // The ID of the tile added to the carousel.
+ uint32 tile_id = 1;
+}
+
+// Event fired when a tile has been removed from the carousel.
+message TileRemoveEvent {
+ // The ID of the tile removed from the carousel.
+ uint32 tile_id = 1;
+}
+
+// Event fired when a tile is swiped to by the user (i.e. it's visible on
+// screen).
+message TileEnterEvent {
+ // The ID of the entered tile.
+ uint32 tile_id = 1;
+}
+
+// Event fired when a tile is swiped away from by the user (i.e. it's no longer
+// visible on screen).
+message TileLeaveEvent {
+ // The ID of the tile.
+ uint32 tile_id = 1;
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/proto/layout.proto b/wear/wear-tiles-renderer/src/androidTest/proto/layout.proto
new file mode 100644
index 0000000..30f6573
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/proto/layout.proto
@@ -0,0 +1,594 @@
+// Composable layout elements that can be combined together to create renderable
+// UI layouts.
+syntax = "proto3";
+
+package androidx.wear.tiles.testing.proto;
+
+import "color.proto";
+import "dimension.proto";
+import "modifiers.proto";
+import "types.proto";
+
+option java_package = "androidx.wear.tiles.testing.proto";
+option java_outer_classname = "LayoutElementProto";
+
+// The horizontal alignment of an element within its container.
+enum HorizontalAlignment {
+ // Horizontal alignment is undefined.
+ HALIGN_UNDEFINED = 0;
+
+ // Horizontally align to the left.
+ HALIGN_LEFT = 1;
+
+ // Horizontally align to center.
+ HALIGN_CENTER = 2;
+
+ // Horizontally align to the right.
+ HALIGN_RIGHT = 3;
+
+ // Horizontally align to the content start (left in LTR layouts, right in RTL
+ // layouts).
+ HALIGN_START = 4;
+
+ // Horizontally align to the content end (right in LTR layouts, left in RTL
+ // layouts).
+ HALIGN_END = 5;
+}
+
+// An extensible HorizontalAlignment property.
+message HorizontalAlignmentProp {
+ // The value
+ HorizontalAlignment value = 1;
+}
+
+// The vertical alignment of an element within its container.
+enum VerticalAlignment {
+ // Vertical alignment is undefined.
+ VALIGN_UNDEFINED = 0;
+
+ // Vertically align to the top.
+ VALIGN_TOP = 1;
+
+ // Vertically align to center.
+ VALIGN_CENTER = 2;
+
+ // Vertically align to the bottom.
+ VALIGN_BOTTOM = 3;
+}
+
+// An extensible VerticalAlignment property.
+message VerticalAlignmentProp {
+ // The value.
+ VerticalAlignment value = 1;
+}
+
+// The weight to be applied to the font.
+enum FontWeight {
+ // Font weight is undefined.
+ FONT_WEIGHT_UNDEFINED = 0;
+
+ // Normal font weight.
+ FONT_WEIGHT_NORMAL = 400;
+
+ // Bold font weight.
+ FONT_WEIGHT_BOLD = 700;
+}
+
+// An extensible FontWeight property.
+message FontWeightProp {
+ // The value.
+ FontWeight value = 1;
+}
+
+// The styling of a font (e.g. font size, and metrics).
+message FontStyle {
+ // The size of the font, in scaled pixels (sp). If not specified, defaults to
+ // the size of the system's "body" font.
+ SpProp size = 1;
+
+ // Whether the text should be rendered in a italic typeface. If not specified,
+ // defaults to "false".
+ BoolProp italic = 2;
+
+ // Whether the text should be rendered with an underline. If not specified,
+ // defaults to "false".
+ BoolProp underline = 3;
+
+ // The text color. If not defined, defaults to white.
+ ColorProp color = 4;
+
+ // The weight of the font. If the provided value is not supported on a
+ // platform, the nearest supported value will be used. If not defined, or
+ // when set to an invalid value, defaults to "normal".
+ FontWeightProp weight = 5;
+
+ // The text letter-spacing. Positive numbers increase the space between
+ // letters while negative numbers tighten the space. If not specified,
+ // defaults to 0.
+ EmProp letter_spacing = 6;
+}
+
+// Alignment of a text element.
+enum TextAlignment {
+ // Alignment is undefined.
+ TEXT_ALIGN_UNDEFINED = 0;
+
+ // Align to the "start" of the Text element (left in LTR layouts, right in
+ // RTL layouts).
+ TEXT_ALIGN_START = 1;
+
+ // Align to the center of the Text element.
+ TEXT_ALIGN_CENTER = 2;
+
+ // Align to the "end" of the Text element (right in LTR layouts, left in RTL
+ // layouts).
+ TEXT_ALIGN_END = 3;
+}
+
+// An extensible TextAlignment property.
+message TextAlignmentProp {
+ // The value.
+ TextAlignment value = 1;
+}
+
+// How text that will not fit inside the bounds of a Text element will be
+// handled.
+//
+// TODO(b/175536688): Rename this to align with Spannable
+enum TextOverflow {
+ // Overflow behavior is undefined.
+ TEXT_OVERFLOW_UNDEFINED = 0;
+
+ // Truncate the text to fit inside of the Text element's bounds. If text is
+ // truncated, it will be truncated on a word boundary.
+ TEXT_OVERFLOW_TRUNCATE = 1;
+
+ // Truncate the text to fit in the Text element's bounds, but add an ellipsis
+ // (i.e. ...) to the end of the text if it has been truncated.
+ TEXT_OVERFLOW_ELLIPSIZE_END = 2;
+}
+
+// An extensible TextOverflow property.
+message TextOverflowProp {
+ // The value.
+ TextOverflow value = 1;
+}
+
+// The anchor position of an Arc's elements. This is used to specify how
+// elements added to an Arc should be laid out with respect to anchor_angle.
+//
+// As an example, assume that the following diagrams are wrapped to an arc, and
+// each represents an Arc element containing a single Text element. The Text
+// element's anchor_angle is "0" for all cases.
+//
+// ```
+// ARC_ANCHOR_START:
+// -180 0 180
+// Hello World!
+//
+//
+// ARC_ANCHOR_CENTER:
+// -180 0 180
+// Hello World!
+//
+// ARC_ANCHOR_END:
+// -180 0 180
+// Hello World!
+// ```
+enum ArcAnchorType {
+ // Anchor position is undefined.
+ ARC_ANCHOR_UNDEFINED = 0;
+
+ // Anchor at the start of the elements. This will cause elements added to an
+ // arc to begin at the given anchor_angle, and sweep around to the right.
+ ARC_ANCHOR_START = 1;
+
+ // Anchor at the center of the elements. This will cause the center of the
+ // whole set of elements added to an arc to be pinned at the given
+ // anchor_angle.
+ ARC_ANCHOR_CENTER = 2;
+
+ // Anchor at the end of the elements. This will cause the set of elements
+ // inside the arc to end at the specified anchor_angle, i.e. all elements
+ // should be to the left of anchor_angle.
+ ARC_ANCHOR_END = 3;
+}
+
+// An extensible ArcAnchorType property.
+message ArcAnchorTypeProp {
+ // The value.
+ ArcAnchorType value = 1;
+}
+
+// A text string.
+message Text {
+ // The text to render.
+ StringProp text = 1;
+
+ // The style of font to use (size, bold etc). If not specified, defaults to
+ // the platform's default body font.
+ FontStyle font_style = 2;
+
+ // Modifiers for this element.
+ Modifiers modifiers = 3;
+
+ // The maximum number of lines that can be represented by the Text element.
+ // If not defined, the Text element will be treated as a single-line element.
+ Int32Prop max_lines = 4;
+
+ // Alignment of the text within its bounds. Note that a Text element will size
+ // itself to wrap its contents, so this option is meaningless for single-line
+ // text (for that, use alignment of the outer container). For multi-line text,
+ // however, this will set the alignment of lines relative to the Text element
+ // bounds. If not defined, defaults to TEXT_ALIGN_CENTER.
+ TextAlignmentProp multiline_alignment = 5;
+
+ // How to handle text which overflows the bound of the Text element.
+ // A Text element will grow as large as possible inside its parent container
+ // (while still respecting max_lines); if it cannot grow large enough to
+ // render all of its text, the text which cannot fit inside its container will
+ // be truncated. If not defined, defaults to TEXT_OVERFLOW_TRUNCATE.
+ TextOverflowProp overflow = 6;
+
+ // The explicit height between lines of text. This is equivalent to the
+ // vertical distance between subsequent baselines. If not specified, defaults
+ // the font's recommended interline spacing.
+ SpProp line_height = 7;
+}
+
+// How content which does not match the dimensions of its bounds (e.g. an image
+// resource being drawn inside an Image) will be resized to fit its bounds.
+enum ContentScaleMode {
+ // Content scaling is undefined.
+ CONTENT_SCALE_MODE_UNDEFINED = 0;
+
+ // Content will be scaled to fit inside its bounds, proportionally. As an
+ // example, If a 10x5 image was going to be drawn inside a 50x50 Image
+ // element, the actual image resource would be drawn as a 50x25 image,
+ // centered within the 50x50 bounds.
+ CONTENT_SCALE_MODE_FIT = 1;
+
+ // Content will be resized proportionally so it completely fills its bounds,
+ // and anything outside of the bounds will be cropped. As an example, if a
+ // 10x5 image was going to be drawn inside a 50x50 Image element, the image
+ // resource would be drawn as a 100x50 image, centered within its bounds (and
+ // with 25px cropped from both the left and right sides).
+ CONTENT_SCALE_MODE_CROP = 2;
+
+ // Content will be resized to fill its bounds, without taking into account the
+ // aspect ratio. If a 10x5 image was going to be drawn inside a 50x50 Image
+ // element, the image would be drawn as a 50x50 image, stretched vertically.
+ CONTENT_SCALE_MODE_FILL_BOUNDS = 3;
+}
+
+// An extensible ContentScaleMode property.
+message ContentScaleModeProp {
+ ContentScaleMode value = 1;
+}
+
+// An image.
+//
+// Images used in this element must exist in the resource bundle that
+// corresponds to this layout. Images must have their dimension specified, and
+// will be rendered at this width and height, regardless of their native
+// dimension.
+message Image {
+ // The resource_id of the image to render. This must exist in the supplied
+ // resource bundle.
+ StringProp resource_id = 1;
+
+ // The width of this image. If not defined, the image will not be rendered.
+ ImageDimension width = 2;
+
+ // The height of this image. If not defined, the image will not be rendered.
+ ImageDimension height = 3;
+
+ // How to scale the image resource inside the bounds specified by width/height
+ // if its size does not match those bounds. Defaults to
+ // CONTENT_SCALE_MODE_FIT.
+ ContentScaleModeProp content_scale_mode = 4;
+
+ // Modifiers for this element.
+ Modifiers modifiers = 5;
+}
+
+// A simple spacer, typically used to provide padding between adjacent elements.
+message Spacer {
+ // The width of this Spacer. When this is added as the direct child of an Arc,
+ // this must be specified as an angular dimension, otherwise a linear
+ // dimension must be used. If not defined, defaults to 0.
+ SpacerDimension width = 1;
+
+ // The height of this spacer. If not defined, defaults to 0.
+ SpacerDimension height = 2;
+
+ // Modifiers for this element.
+ Modifiers modifiers = 3;
+}
+
+// A container which stacks all of its children on top of one another. This also
+// allows to add a background color, or to have a border around them with some
+// padding.
+message Box {
+ // The child element(s) to wrap.
+ repeated LayoutElement contents = 1;
+
+ // The height of this Box. If not defined, this will size itself to fit all of
+ // its children (i.e. a WrappedDimension).
+ ContainerDimension height = 2;
+
+ // The width of this Box. If not defined, this will size itself to fit all of
+ // its children (i.e. a WrappedDimension).
+ ContainerDimension width = 3;
+
+ // The horizontal alignment of the element inside this Box. If not defined,
+ // defaults to HALIGN_CENTER.
+ HorizontalAlignmentProp horizontal_alignment = 4;
+
+ // The vertical alignment of the element inside this Box. If not defined,
+ // defaults to VALIGN_CENTER.
+ VerticalAlignmentProp vertical_alignment = 5;
+
+ // Modifiers for this element.
+ Modifiers modifiers = 6;
+}
+
+// A portion of text which can be added to a Span. Two different SpanText
+// elements on the same line will be aligned to the same baseline, regardless of
+// the size of each SpanText.
+message SpanText {
+ // The text to render.
+ StringProp text = 1;
+
+ // The style of font to use (size, bold etc). If not specified, defaults to
+ // the platform's default body font.
+ FontStyle font_style = 2;
+
+ // Modifiers for this element.
+ SpanModifiers modifiers = 3;
+}
+
+// An image which can be added to a Span.
+message SpanImage {
+ // The resource_id of the image to render. This must exist in the supplied
+ // resource bundle.
+ StringProp resource_id = 1;
+
+ // The width of this image. If not defined, the image will not be rendered.
+ DpProp width = 2;
+
+ // The height of this image. If not defined, the image will not be rendered.
+ DpProp height = 3;
+
+ // Modifiers for this element.
+ SpanModifiers modifiers = 4;
+}
+
+// A single Span. Each Span forms part of a larger Spannable widget. At the
+// moment, the only widgets which can be added to Spannable containers are
+// SpanText and SpanImage elements.
+message Span {
+ oneof inner {
+ SpanText text = 1;
+ SpanImage image = 2;
+ }
+}
+
+// A container of Span elements. Currently, this only supports Text elements,
+// where each individual Span can have different styling applied to it but the
+// resulting text will flow naturally. This allows sections of a paragraph of
+// text to have different styling applied to it, for example, making one or two
+// words bold or italic.
+message Spannable {
+ // The Span elements that form this Spannable.
+ repeated Span spans = 1;
+
+ // Modifiers for this element.
+ Modifiers modifiers = 2;
+
+ // The maximum number of lines that can be represented by the Spannable
+ // element. If not defined, the Spannable element will be treated as a
+ // single-line element.
+ Int32Prop max_lines = 3;
+
+ // Alignment of the Spannable content within its bounds. Note that a Spannable
+ // element will size itself to wrap its contents, so this option is
+ // meaningless for single-line content (for that, use alignment of the outer
+ // container). For multi-line content, however, this will set the alignment of
+ // lines relative to the Spannable element bounds. If not defined, defaults to
+ // TEXT_ALIGN_CENTER.
+ HorizontalAlignmentProp multiline_alignment = 4;
+
+ // How to handle content which overflows the bound of the Spannable element.
+ // A Spannable element will grow as large as possible inside its parent
+ // container (while still respecting max_lines); if it cannot grow large
+ // enough to render all of its content, the content which cannot fit inside
+ // its container will be truncated. If not defined, defaults to
+ // TEXT_OVERFLOW_TRUNCATE.
+ TextOverflowProp overflow = 5;
+
+ // Extra spacing to add between each line. This will apply to all
+ // spans regardless of their font size. This is in addition to original
+ // line heights. Note that this won't add any additional space before the
+ // first line or after the last line. The default value is zero and negative
+ // values will decrease the interline spacing.
+ SpProp line_spacing = 6;
+}
+
+// A column of elements. Each child element will be laid out vertically, one
+// after another (i.e. stacking down). This element will size itself to the
+// smallest size required to hold all of its children (e.g. if it contains three
+// elements sized 10x10, 20x20 and 30x30, the resulting column will be 30x60).
+//
+// If specified, horizontal_alignment can be used to control the gravity inside
+// the container, affecting the horizontal placement of children whose width are
+// smaller than the resulting column width.
+message Column {
+ // The list of child elements to place inside this Column.
+ repeated LayoutElement contents = 1;
+
+ // The horizontal alignment of elements inside this column, if they are
+ // narrower than the resulting width of the column. If not defined, defaults
+ // to HALIGN_CENTER.
+ HorizontalAlignmentProp horizontal_alignment = 2;
+
+ // The width of this column. If not defined, this will size itself to fit
+ // all of its children (i.e. a WrappedDimension).
+ ContainerDimension width = 3;
+
+ // The height of this column. If not defined, this will size itself to fit
+ // all of its children (i.e. a WrappedDimension).
+ ContainerDimension height = 4;
+
+ // Modifiers for this element.
+ Modifiers modifiers = 5;
+}
+
+// A row of elements. Each child will be laid out horizontally, one after
+// another (i.e. stacking to the right). This element will size itself to the
+// smallest size required to hold all of its children (e.g. if it contains three
+// elements sized 10x10, 20x20 and 30x30, the resulting row will be 60x30).
+//
+// If specified, vertical_alignment can be used to control the gravity inside
+// the container, affecting the vertical placement of children whose width are
+// smaller than the resulting row height.
+message Row {
+ // The list of child elements to place inside this Row.
+ repeated LayoutElement contents = 1;
+
+ // The vertical alignment of elements inside this row, if they are narrower
+ // than the resulting height of the row. If not defined, defaults to
+ // VALIGN_CENTER.
+ VerticalAlignmentProp vertical_alignment = 2;
+
+ // The width of this row. If not defined, this will size itself to fit
+ // all of its children (i.e. a WrappedDimension).
+ ContainerDimension width = 3;
+
+ // The height of this row. If not defined, this will size itself to fit
+ // all of its children (i.e. a WrappedDimension).
+ ContainerDimension height = 4;
+
+ // Modifiers for this element.
+ Modifiers modifiers = 5;
+}
+
+// An arc container. This container will fill itself to a circle, which fits
+// inside its parent container, and all of its children will be placed on that
+// circle. The fields anchor_angle and anchor_type can be used to specify where
+// to draw children within this circle.
+message Arc {
+ // Contents of this container.
+ repeated ArcLayoutElement contents = 1;
+
+ // The angle for the anchor, used with anchor_type to determine where to draw
+ // children. Note that 0 degrees is the 12 o clock position on a device, and
+ // the angle sweeps clockwise. If not defined, defaults to 0 degrees.
+ //
+ // Values do not have to be clamped to the range 0-360; values less than 0
+ // degrees will sweep anti-clockwise (i.e. -90 degrees is equivalent to 270
+ // degrees), and values >360 will be be placed at X mod 360 degrees.
+ DegreesProp anchor_angle = 2;
+
+ // How to align the contents of this container relative to anchor_angle. See
+ // the descriptions of options in ArcAnchorType for more information. If not
+ // defined, defaults to ARC_ANCHOR_CENTER.
+ ArcAnchorTypeProp anchor_type = 3;
+
+ // Vertical alignment of elements within the arc. If the Arc's thickness is
+ // larger than the thickness of the element being drawn, this controls whether
+ // the element should be drawn towards the inner or outer edge of the arc, or
+ // drawn in the center.
+ // If not defined, defaults to VALIGN_CENTER
+ VerticalAlignmentProp vertical_align = 4;
+
+ // Modifiers for this element.
+ Modifiers modifiers = 5;
+}
+
+// A text element that can be used in an Arc.
+message ArcText {
+ // The text to render.
+ StringProp text = 1;
+
+ // The style of font to use (size, bold etc). If not specified, defaults to
+ // the platform's default body font.
+ FontStyle font_style = 2;
+
+ // Modifiers for this element.
+ ArcModifiers modifiers = 3;
+}
+
+// A line that can be used in an Arc and renders as a round progress bar.
+message ArcLine {
+ // The length of this line, in degrees. If not defined, defaults to 0.
+ DegreesProp length = 1;
+
+ // The thickness of this line. If not defined, defaults to 0.
+ DpProp thickness = 2;
+
+ // The color of this line.
+ ColorProp color = 3;
+
+ // Modifiers for this element.
+ ArcModifiers modifiers = 4;
+}
+
+// A simple spacer used to provide padding between adjacent elements in an Arc.
+message ArcSpacer {
+ // The length of this spacer, in degrees. If not defined, defaults to 0.
+ DegreesProp length = 1;
+
+ // The thickness of this spacer, in DP. If not defined, defaults to 0.
+ DpProp thickness = 2;
+
+ // Modifiers for this element.
+ ArcModifiers modifiers = 3;
+}
+
+// A container that allows a standard LayoutElement to be added to an Arc.
+message ArcAdapter {
+ // The element to adapt to an Arc.
+ LayoutElement content = 1;
+
+ // Whether this adapter's contents should be rotated, according to its
+ // position in the arc or not. As an example, assume that an Image has been
+ // added to the arc, and ends up at the 3 o clock position. If rotate_contents
+ // = true, the image will be placed at the 3 o clock position, and will be
+ // rotated clockwise through 90 degrees. If rotate_contents = false, the image
+ // will be placed at the 3 o clock position, but itself will not be rotated.
+ // If not defined, defaults to false.
+ BoolProp rotate_contents = 2;
+}
+
+// The root of all layout elements. This exists to act as a holder for all of
+// the actual layout elements above.
+message LayoutElement {
+ oneof inner {
+ Column column = 1;
+ Row row = 2;
+ Box box = 3;
+ Spacer spacer = 4;
+ Text text = 5;
+ Image image = 6;
+ Arc arc = 7;
+ Spannable spannable = 8;
+ }
+}
+
+// The root of all elements that can be used in an Arc. This exists to act as a
+// holder for all of the actual arc layout elements above.
+message ArcLayoutElement {
+ oneof inner {
+ ArcText text = 1;
+ ArcLine line = 2;
+ ArcSpacer spacer = 3;
+ ArcAdapter adapter = 4;
+ }
+}
+
+// A complete layout.
+message Layout {
+ // The root element in the layout.
+ LayoutElement root = 1;
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/proto/modifiers.proto b/wear/wear-tiles-renderer/src/androidTest/proto/modifiers.proto
new file mode 100644
index 0000000..e4b52b9
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/proto/modifiers.proto
@@ -0,0 +1,125 @@
+// Modifiers for composable layout elements.
+syntax = "proto3";
+
+package androidx.wear.tiles.testing.proto;
+
+import "action.proto";
+import "color.proto";
+import "dimension.proto";
+import "types.proto";
+
+option java_package = "androidx.wear.tiles.testing.proto";
+option java_outer_classname = "ModifiersProto";
+
+// A modifier for an element which can have associated Actions for click events.
+// When an element with a ClickableModifier is clicked it will fire the
+// associated action.
+message Clickable {
+ // The ID associated with this action.
+ string id = 1;
+
+ // The action to perform when the element this modifier is attached to is
+ // clicked.
+ Action on_click = 2;
+}
+
+// A modifier for an element which has accessibility semantics associated with
+// it. This should generally be used sparingly, and in most cases should only be
+// applied to the top-level layout element or to Clickables.
+message Semantics {
+ // The content description associated with this element. This will be dictated
+ // when the element is focused by the screen reader.
+ string content_description = 1;
+}
+
+// A modifier to apply padding around an element.
+message Padding {
+ // The padding on the end of the content, depending on the layout direction,
+ // in DP and the value of "rtl_aware".
+ DpProp end = 1;
+
+ // The padding on the start of the content, depending on the layout direction,
+ // in DP and the value of "rtl_aware".
+ DpProp start = 2;
+
+ // The padding at the top, in DP.
+ DpProp top = 3;
+
+ // The padding at the bottom, in DP.
+ DpProp bottom = 4;
+
+ // Whether the start/end padding is aware of RTL support. If true, the values
+ // for start/end will follow the layout direction (i.e. start will refer to
+ // the right hand side of the container if the device is using an RTL locale).
+ // If false, start/end will always map to left/right, accordingly.
+ BoolProp rtl_aware = 5;
+}
+
+// A modifier to apply a border around an element.
+message Border {
+ // The width of the border, in DP.
+ DpProp width = 1;
+
+ // The color of the border.
+ ColorProp color = 2;
+}
+
+// The corner of a Box element.
+message Corner {
+ // The radius of the corner in DP.
+ DpProp radius = 1;
+}
+
+// A modifier to apply a background to an element.
+message Background {
+ // The background color for this element. If not defined, defaults to being
+ // transparent.
+ ColorProp color = 1;
+
+ // The corner properties of this element. This only affects the drawing of
+ // this element if it has a background color or border. If not defined,
+ // defaults to having a square corner.
+ Corner corner = 2;
+}
+
+// Modifiers for an element. These may change the way they are drawn (e.g.
+// Padding or Background), or change their behaviour (e.g. Clickable, or
+// Semantics).
+message Modifiers {
+ // Allows its wrapped element to have actions associated with it, which will
+ // be executed when the element is tapped.
+ Clickable clickable = 1;
+
+ // Adds metadata for the modified element, for example, screen reader content
+ // descriptions.
+ Semantics semantics = 2;
+
+ // Adds padding to the modified element.
+ Padding padding = 3;
+
+ // Draws a border around the modified element.
+ Border border = 4;
+
+ // Adds a background (with optional corner radius) to the modified element.
+ Background background = 5;
+}
+
+// Modifiers that can be used with ArcLayoutElements. These may change the way
+// they are drawn, or change their behaviour.
+message ArcModifiers {
+ // Allows its wrapped element to have actions associated with it, which will
+ // be executed when the element is tapped.
+ Clickable clickable = 1;
+
+ // Adds metadata for the modified element, for example, screen reader content
+ // descriptions.
+ Semantics semantics = 2;
+}
+
+// Modifiers that can be used with Span elements. These may change the way
+// they are drawn, or change their behaviour.
+message SpanModifiers {
+ // Allows its wrapped element to have actions associated with it, which will
+ // be executed when the element is tapped.
+ Clickable clickable = 1;
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/proto/requests.proto b/wear/wear-tiles-renderer/src/androidTest/proto/requests.proto
new file mode 100644
index 0000000..2075e7c
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/proto/requests.proto
@@ -0,0 +1,34 @@
+// Request messages used to fetch tiles and resources
+syntax = "proto3";
+
+package androidx.wear.tiles.testing.proto;
+
+import "device_parameters.proto";
+import "state.proto";
+
+option java_package = "androidx.wear.tiles.testing.proto";
+option java_outer_classname = "RequestProto";
+
+// Parameters passed to a Tile provider when the renderer is requesting a new
+// version of the tile.
+message TileRequest {
+ // Parameters describing the device requesting the tile update.
+ DeviceParameters device_parameters = 1;
+
+ // The state that should be used when building the tile.
+ State state = 2;
+}
+
+// Parameters passed to a Tile provider when the renderer is requesting a
+// specific resource version.
+message ResourcesRequest {
+ // The version of the resources being fetched
+ string version = 1;
+
+ // Requested resource IDs. If not specified, all resources for the given
+ // version must be provided in the response.
+ repeated string resource_ids = 2;
+
+ // Parameters describing the device requesting the resources.
+ DeviceParameters device_parameters = 3;
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/proto/resources.proto b/wear/wear-tiles-renderer/src/androidTest/proto/resources.proto
new file mode 100644
index 0000000..d322ede
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/proto/resources.proto
@@ -0,0 +1,73 @@
+// The resources for a layout.
+syntax = "proto3";
+
+package androidx.wear.tiles.testing.proto;
+
+
+option java_package = "androidx.wear.tiles.testing.proto";
+option java_outer_classname = "ResourceProto";
+
+// Format describing the contents of an image data byte array.
+enum ImageFormat {
+ // An undefined image format.
+ IMAGE_FORMAT_UNDEFINED = 0;
+
+ // An image format where each pixel is stored on 2 bytes, with red using 5
+ // bits, green using 6 bits and blue using 5 bits of precision.
+ IMAGE_FORMAT_RGB_565 = 1;
+}
+
+// An image resource which maps to an Android drawable by resource ID.
+message AndroidImageResourceByResId {
+ // The Android resource ID of this image. This must refer to a drawable under
+ // R.drawable.
+ int32 resource_id = 1;
+}
+
+// An image resource whose data is fully inlined, with no dependency on a
+// system or app resource.
+message InlineImageResource {
+ // The byte array representing the image.
+ bytes data = 1;
+
+ // The native width of the image, in pixels. Only required for formats
+ // (e.g. IMAGE_FORMAT_RGB_565) where the image data does not include size.
+ int32 width_px = 2;
+
+ // The native height of the image, in pixels. Only required for formats
+ // (e.g. IMAGE_FORMAT_RGB_565) where the image data does not include size.
+ int32 height_px = 3;
+
+ // The format of the byte array data representing the image. May be left
+ // unspecified or set to IMAGE_FORMAT_UNDEFINED in which case the platform
+ // will attempt to extract this from the raw image data. If the platform does
+ // not support the format, the image will not be decoded or displayed.
+ ImageFormat format = 4;
+}
+
+// An image resource, which can be used by layouts. This holds multiple
+// underlying resource types, which the underlying runtime will pick according
+// to what it thinks is appropriate.
+message ImageResource {
+ // An image resource that maps to an Android drawable by resource ID.
+ AndroidImageResourceByResId android_resource_by_resid = 1;
+
+ // An image resource that contains the image data inline.
+ InlineImageResource inline_resource = 2;
+}
+
+// The resources for a layout.
+message Resources {
+ // The version of this Resources instance.
+ //
+ // Each tile specifies the version of resources it requires. After fetching a
+ // tile, the renderer will use the resources version specified by the tile
+ // to separately fetch the resources.
+ //
+ // This value must match the version of the resources required by the tile
+ // for the tile to render successfully.
+ string version = 1;
+
+ // A map of resource_ids to images, which can be used by layouts.
+ map<string, ImageResource> id_to_image = 2;
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/proto/state.proto b/wear/wear-tiles-renderer/src/androidTest/proto/state.proto
new file mode 100644
index 0000000..b771ec3
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/proto/state.proto
@@ -0,0 +1,14 @@
+// State of a tile.
+syntax = "proto3";
+
+package androidx.wear.tiles.testing.proto;
+
+
+option java_package = "androidx.wear.tiles.testing.proto";
+option java_outer_classname = "StateProto";
+
+// State information.
+message State {
+ // The ID of the clickable that was last clicked.
+ string last_clickable_id = 1;
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/proto/tile.proto b/wear/wear-tiles-renderer/src/androidTest/proto/tile.proto
new file mode 100644
index 0000000..8831717
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/proto/tile.proto
@@ -0,0 +1,30 @@
+// The components of a tile that can be rendered by a tile renderer.
+syntax = "proto3";
+
+package androidx.wear.tiles.testing.proto;
+
+import "timeline.proto";
+import "version.proto";
+
+option java_package = "androidx.wear.tiles.testing.proto";
+option java_outer_classname = "TileProto";
+
+// A holder for a tile. This specifies the resources to use for this delivery
+// of the tile, and the timeline for the tile.
+message Tile {
+ // The resource version required for these tiles.
+ string resources_version = 1;
+
+ // The tiles to show in the carousel, along with their validity periods.
+ Timeline timeline = 2;
+
+ // The schema version that this tile was built with.
+ VersionInfo schema_version = 3;
+
+ // How many milliseconds of elapsed time (**not** wall clock time) this tile
+ // can be considered to be "fresh". The platform will attempt to refresh
+ // your tile at some point in the future after this interval has lapsed. A
+ // value of 0 here signifies that auto-refreshes should not be used (i.e. you
+ // will manually request updates via TileProviderService#getRequester).
+ uint64 freshness_interval_millis = 4;
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/proto/timeline.proto b/wear/wear-tiles-renderer/src/androidTest/proto/timeline.proto
new file mode 100644
index 0000000..8aeb4fe
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/proto/timeline.proto
@@ -0,0 +1,46 @@
+// A timeline with entries representing content that should be displayed within
+// given time intervals.
+syntax = "proto3";
+
+package androidx.wear.tiles.testing.proto;
+
+import "layout.proto";
+
+option java_package = "androidx.wear.tiles.testing.proto";
+option java_outer_classname = "TimelineProto";
+
+// A time interval, typically used to describe the validity period of a
+// TimelineEntry
+message TimeInterval {
+ // Starting point of the time interval, in milliseconds since the Unix epoch.
+ int64 start_millis = 1;
+
+ // End point of the time interval, in milliseconds since the Unix epoch.
+ int64 end_millis = 2;
+}
+
+// One piece of renderable content along with the time that it is valid for.
+message TimelineEntry {
+ // The validity period for this timeline entry.
+ TimeInterval validity = 1;
+
+ // The contents of this timeline entry.
+ Layout layout = 2;
+}
+
+// A collection of TimelineEntry items.
+//
+// TimelineEntry items can be used to update a layout on-screen at known times,
+// without having to explicitly update a layout. This allows for cases where,
+// say, a calendar can be used to show the next event, and automatically switch
+// to showing the next event when one has passed.
+//
+// The active TimelineEntry is switched, at most, once a minute. In the case
+// where the validity periods of TimelineEntry items overlap, the item with the
+// *shortest* validity period will be shown. This allows a layout provider to
+// show a "default" layout, and override it at set points without having to
+// explicitly insert the default layout between the "override" layout.
+message Timeline {
+ // The entries in a timeline.
+ repeated TimelineEntry timeline_entries = 1;
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/proto/types.proto b/wear/wear-tiles-renderer/src/androidTest/proto/types.proto
new file mode 100644
index 0000000..5a224d6
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/proto/types.proto
@@ -0,0 +1,31 @@
+// Extensible primitive types used by layout elements.
+syntax = "proto3";
+
+package androidx.wear.tiles.testing.proto;
+
+option java_package = "androidx.wear.tiles.testing.proto";
+option java_outer_classname = "TypesProto";
+
+// An int32 type.
+message Int32Prop {
+ // The value.
+ int32 value = 1;
+}
+
+// A string type.
+message StringProp {
+ // The value.
+ string value = 1;
+}
+
+// A float type.
+message FloatProp {
+ // The value.
+ float value = 1;
+}
+
+// A boolean type.
+message BoolProp {
+ // The value.
+ bool value = 1;
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/proto/version.proto b/wear/wear-tiles-renderer/src/androidTest/proto/version.proto
new file mode 100644
index 0000000..76bf0d9
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/proto/version.proto
@@ -0,0 +1,21 @@
+// The components of a tile that can be rendered by a tile renderer.
+syntax = "proto3";
+
+package androidx.wear.tiles.testing.proto;
+
+
+option java_package = "androidx.wear.tiles.testing.proto";
+option java_outer_classname = "VersionProto";
+
+// Version information. This is used to encode the schema version of a payload
+// (e.g. inside of Tile).
+message VersionInfo {
+ // Major version. Incremented on breaking changes (i.e. compatibility is not
+ // guaranteed across major versions).
+ uint32 major = 1;
+
+ // Minor version. Incremented on non-breaking changes (e.g. schema additions).
+ // Anything consuming a payload can safely consume anything with a lower
+ // minor version.
+ uint32 minor = 2;
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/drawable/android_24dp.xml b/wear/wear-tiles-renderer/src/androidTest/res/drawable/android_24dp.xml
new file mode 100644
index 0000000..17ebdea
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/drawable/android_24dp.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#FF000000" android:pathData="M17.6,11.48 L19.44,8.3a0.63,0.63 0,0 0,-1.09 -0.63l-1.88,3.24a11.43,11.43 0,0 0,-8.94 0L5.65,7.67a0.63,0.63 0,0 0,-1.09 0.63L6.4,11.48A10.81,10.81 0,0 0,1 20L23,20A10.81,10.81 0,0 0,17.6 11.48ZM7,17.25A1.25,1.25 0,1 1,8.25 16,1.25 1.25,0 0,1 7,17.25ZM17,17.25A1.25,1.25 0,1 1,18.25 16,1.25 1.25,0 0,1 17,17.25Z"/>
+</vector>
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/drawable/broken_drawable.xml b/wear/wear-tiles-renderer/src/androidTest/res/drawable/broken_drawable.xml
new file mode 100644
index 0000000..04ead12
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/drawable/broken_drawable.xml
@@ -0,0 +1 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="21dp" android:height="29.7dp" android:viewportWidth="210" android:viewportHeight="297"></vector>
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/drawable/filled_image.png b/wear/wear-tiles-renderer/src/androidTest/res/drawable/filled_image.png
new file mode 100644
index 0000000..a6da988
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/drawable/filled_image.png
Binary files differ
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/drawable/ic_channel_foreground.xml b/wear/wear-tiles-renderer/src/androidTest/res/drawable/ic_channel_foreground.xml
new file mode 100644
index 0000000..399d346
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/drawable/ic_channel_foreground.xml
@@ -0,0 +1,15 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="120dp"
+ android:height="120dp"
+ android:viewportWidth="120"
+ android:viewportHeight="120"
+ android:tint="#FFFFFF">
+ <group android:scaleX="2.9"
+ android:scaleY="2.9"
+ android:translateX="25.2"
+ android:translateY="25.2">
+ <path
+ android:pathData="M17.6,11.48 L19.44,8.3a0.63,0.63 0,0 0,-1.09 -0.63l-1.88,3.24a11.43,11.43 0,0 0,-8.94 0L5.65,7.67a0.63,0.63 0,0 0,-1.09 0.63L6.4,11.48A10.81,10.81 0,0 0,1 20L23,20A10.81,10.81 0,0 0,17.6 11.48ZM7,17.25A1.25,1.25 0,1 1,8.25 16,1.25 1.25,0 0,1 7,17.25ZM17,17.25A1.25,1.25 0,1 1,18.25 16,1.25 1.25,0 0,1 17,17.25Z"
+ android:fillColor="#FF000000"/>
+ </group>
+</vector>
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/mipmap-anydpi-v26/android_withbg_120dp.xml b/wear/wear-tiles-renderer/src/androidTest/res/mipmap-anydpi-v26/android_withbg_120dp.xml
new file mode 100644
index 0000000..5c4c23a
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/mipmap-anydpi-v26/android_withbg_120dp.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@color/ic_channel_background"/>
+ <foreground android:drawable="@drawable/ic_channel_foreground"/>
+</adaptive-icon>
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/all_modifiers.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/all_modifiers.textproto
new file mode 100644
index 0000000..ac2b253
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/all_modifiers.textproto
@@ -0,0 +1,41 @@
+# Note: this test is really an analog of box_with_corners_and_border. This
+# should produce an identical result.
+text {
+ text {
+ value: "Hello World"
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFF00FF00
+ }
+ corner {
+ radius {
+ value: 5
+ }
+ }
+ }
+ padding {
+ end {
+ value: 10
+ }
+ top {
+ value: 20
+ }
+ bottom {
+ value: 30
+ }
+ start {
+ value: 40
+ }
+ }
+ border {
+ color {
+ argb: 0xFFFF0000
+ }
+ width {
+ value: 5
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_above_360.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_above_360.textproto
new file mode 100644
index 0000000..7573de9
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_above_360.textproto
@@ -0,0 +1,76 @@
+box {
+ modifiers {
+ background {
+ color {
+ argb: 0xFF000000
+ }
+ }
+ }
+ width {
+ expanded_dimension {}
+ }
+ height {
+ expanded_dimension {}
+ }
+
+ contents {
+ arc {
+ contents {
+ line {
+ length {
+ value: 360
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ thickness {
+ value: 11
+ }
+ }
+ }
+ }
+ }
+ contents {
+ box {
+ width {
+ expanded_dimension {}
+ }
+ height {
+ expanded_dimension {}
+ }
+ modifiers {
+ padding {
+ top {
+ value: 15
+ }
+ end {
+ value: 15
+ }
+ bottom {
+ value: 15
+ }
+ start {
+ value: 15
+ }
+ }
+ }
+ contents {
+ arc {
+ contents {
+ line {
+ length {
+ value: 750
+ }
+ color {
+ argb: 0xFF00FF00
+ }
+ thickness {
+ value: 11
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_alignment.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_alignment.textproto
new file mode 100644
index 0000000..a9edc23
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_alignment.textproto
@@ -0,0 +1,213 @@
+box {
+ width {
+ expanded_dimension {}
+ }
+ height {
+ expanded_dimension {}
+ }
+
+ contents {
+ arc {
+ anchor_type: {
+ value: ARC_ANCHOR_START
+ }
+ anchor_angle: {
+ value: 0
+ }
+ vertical_align {
+ value: VALIGN_TOP
+ }
+ contents {
+ text {
+ text {
+ value: "TOP"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ length {
+ value: 15
+ }
+ }
+ }
+ contents {
+ line {
+ color {
+ argb: 0xFF00FF00
+ }
+ length {
+ value: 10
+ }
+ thickness {
+ value: 50
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ arc {
+ anchor_type: {
+ value: ARC_ANCHOR_START
+ }
+ anchor_angle: {
+ value: 90
+ }
+ vertical_align {
+ value: VALIGN_CENTER
+ }
+ contents {
+ text {
+ text {
+ value: "CENTER"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ length {
+ value: 15
+ }
+ }
+ }
+ contents {
+ line {
+ color {
+ argb: 0xFF00FF00
+ }
+ length {
+ value: 10
+ }
+ thickness {
+ value: 50
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ arc {
+ anchor_type: {
+ value: ARC_ANCHOR_START
+ }
+ anchor_angle: {
+ value: 180
+ }
+ vertical_align {
+ value: VALIGN_BOTTOM
+ }
+ contents {
+ text {
+ text {
+ value: "BOT"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ length {
+ value: 15
+ }
+ }
+ }
+ contents {
+ line {
+ color {
+ argb: 0xFF00FF00
+ }
+ length {
+ value: 10
+ }
+ thickness {
+ value: 50
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ arc {
+ anchor_type: {
+ value: ARC_ANCHOR_START
+ }
+ anchor_angle: {
+ value: 270
+ }
+ contents {
+ text {
+ text {
+ value: "DEF"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ length {
+ value: 15
+ }
+ }
+ }
+ contents {
+ line {
+ color {
+ argb: 0xFF00FF00
+ }
+ length {
+ value: 10
+ }
+ thickness {
+ value: 50
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_alignment_mixed_types.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_alignment_mixed_types.textproto
new file mode 100644
index 0000000..fc6e0e8
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_alignment_mixed_types.textproto
@@ -0,0 +1,374 @@
+box {
+ width {
+ expanded_dimension {}
+ }
+ height {
+ expanded_dimension {}
+ }
+
+ contents {
+ arc {
+ anchor_type: {
+ value: ARC_ANCHOR_START
+ }
+ anchor_angle: {
+ value: 0
+ }
+ vertical_align {
+ value: VALIGN_TOP
+ }
+ contents {
+ adapter {
+ content {
+ box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ corner {
+ radius {
+ value: 12
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "T"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFFFFFF
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ length {
+ value: 20
+ }
+ }
+ }
+ contents {
+ line {
+ color {
+ argb: 0xFF00FF00
+ }
+ length {
+ value: 10
+ }
+ thickness {
+ value: 60
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ arc {
+ anchor_type: {
+ value: ARC_ANCHOR_START
+ }
+ anchor_angle: {
+ value: 90
+ }
+ vertical_align {
+ value: VALIGN_CENTER
+ }
+ contents {
+ adapter {
+ content {
+ box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ corner {
+ radius {
+ value: 12
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "C"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFFFFFF
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ length {
+ value: 20
+ }
+ }
+ }
+ contents {
+ line {
+ color {
+ argb: 0xFF00FF00
+ }
+ length {
+ value: 10
+ }
+ thickness {
+ value: 60
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ arc {
+ anchor_type: {
+ value: ARC_ANCHOR_START
+ }
+ anchor_angle: {
+ value: 180
+ }
+ vertical_align {
+ value: VALIGN_BOTTOM
+ }
+ contents {
+ adapter {
+ content {
+ box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ corner {
+ radius {
+ value: 12
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "B"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFFFFFF
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ length {
+ value: 20
+ }
+ }
+ }
+ contents {
+ line {
+ color {
+ argb: 0xFF00FF00
+ }
+ length {
+ value: 10
+ }
+ thickness {
+ value: 60
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ arc {
+ anchor_type: {
+ value: ARC_ANCHOR_START
+ }
+ anchor_angle: {
+ value: 270
+ }
+ contents {
+ adapter {
+ content {
+ box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ corner {
+ radius {
+ value: 12
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "DEF"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFFFFFF
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ length {
+ value: 20
+ }
+ }
+ }
+ contents {
+ line {
+ color {
+ argb: 0xFF00FF00
+ }
+ length {
+ value: 10
+ }
+ thickness {
+ value: 60
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_anchors.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_anchors.textproto
new file mode 100644
index 0000000..220ba5e
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_anchors.textproto
@@ -0,0 +1,140 @@
+box {
+ width {
+ expanded_dimension {}
+ }
+ height {
+ expanded_dimension {}
+ }
+ contents {
+ arc {
+ anchor_angle: {
+ value: 0
+ }
+ anchor_type {
+ value: ARC_ANCHOR_END
+ }
+ contents {
+ text {
+ text {
+ value: "ANCHOR_END - 0"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ arc {
+ anchor_angle: {
+ value: 0
+ }
+ anchor_type: {
+ value: ARC_ANCHOR_START
+ }
+ contents {
+ text {
+ text {
+ value: "ANCHOR_START - 0"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ arc {
+ anchor_angle: {
+ value: 180
+ }
+ anchor_type {
+ value: ARC_ANCHOR_CENTER
+ }
+ contents {
+ text {
+ text {
+ value: "ANCHOR_CENTER - 180"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ box {
+ modifiers {
+ padding {
+ start {
+ value: 30
+ }
+ end {
+ value: 30
+ }
+ top {
+ value: 30
+ }
+ bottom {
+ value: 30
+ }
+ }
+ }
+ contents {
+ arc {
+ anchor_angle: {
+ value: 0
+ }
+ contents {
+ text {
+ text {
+ value: "ANCHOR_DEFAULT - 0"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_text_and_lines.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_text_and_lines.textproto
new file mode 100644
index 0000000..e00b3e0
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_text_and_lines.textproto
@@ -0,0 +1,57 @@
+arc {
+ anchor_type: {
+ value: ARC_ANCHOR_CENTER
+ }
+ anchor_angle: {
+ value: 0
+ }
+ contents {
+ text {
+ text {
+ value: "HELLO"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ }
+ contents {
+ line {
+ color {
+ argb: 0xFF00FF00
+ }
+ length {
+ value: 45
+ }
+ thickness {
+ value: 11
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "WORLD"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFF0000FF
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_with_buttons_rotated.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_with_buttons_rotated.textproto
new file mode 100644
index 0000000..69d7804
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_with_buttons_rotated.textproto
@@ -0,0 +1,374 @@
+arc {
+ anchor_angle: {
+ value: 0
+ }
+ anchor_type: {
+ value: ARC_ANCHOR_CENTER
+ }
+ contents {
+ adapter {
+ rotate_contents: {
+ value: true
+ }
+ content {
+ box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ corner {
+ radius {
+ value: 12
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "1"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFFFFFF
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ adapter {
+ rotate_contents: {
+ value: true
+ }
+ content {
+ box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFF00FF00
+ }
+ corner {
+ radius {
+ value: 12
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "2"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFFFFFF
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ adapter {
+ content {
+ box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFF0000FF
+ }
+ corner {
+ radius {
+ value: 12
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "3"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFFFFFF
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ adapter {
+ rotate_contents: {
+ value: true
+ }
+ content {
+ box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF00FF
+ }
+ corner {
+ radius {
+ value: 12
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "4"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFFFFFF
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ adapter {
+ rotate_contents: {
+ value: true
+ }
+ content {
+ box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFF00FFFF
+ }
+ corner {
+ radius {
+ value: 12
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "5"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFFFFFF
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ adapter {
+ rotate_contents: {
+ value: true
+ }
+ content {
+ box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFFFF00
+ }
+ corner {
+ radius {
+ value: 12
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "6"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFFFFFF
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_with_buttons_unrotated.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_with_buttons_unrotated.textproto
new file mode 100644
index 0000000..d30ba9e
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/arc_with_buttons_unrotated.textproto
@@ -0,0 +1,360 @@
+arc {
+ anchor_angle: {
+ value: 0
+ }
+ anchor_type: {
+ value: ARC_ANCHOR_CENTER
+ }
+
+ contents {
+ adapter {
+ content {
+ box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ corner {
+ radius {
+ value: 12
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "1"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFFFFFF
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ adapter {
+ content {
+ box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFF00FF00
+ }
+ corner {
+ radius {
+ value: 12
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "2"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFFFFFF
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ adapter {
+ content {
+ box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFF0000FF
+ }
+ corner {
+ radius {
+ value: 12
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "3"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFFFFFF
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ adapter {
+ content {
+ box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF00FF
+ }
+ corner {
+ radius {
+ value: 12
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "4"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFFFFFF
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ adapter {
+ content {
+ box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFF00FFFF
+ }
+ corner {
+ radius {
+ value: 12
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "5"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFFFFFF
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ adapter {
+ content {
+ box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFFFF00
+ }
+ corner {
+ radius {
+ value: 12
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "6"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFFFFFF
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/box_with_corners_and_border.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/box_with_corners_and_border.textproto
new file mode 100644
index 0000000..4011e59
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/box_with_corners_and_border.textproto
@@ -0,0 +1,43 @@
+box {
+ contents {
+ text {
+ text {
+ value: "Hello World"
+ }
+ }
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFF00FF00
+ }
+ corner {
+ radius {
+ value: 5
+ }
+ }
+ }
+ padding {
+ end {
+ value: 10
+ }
+ top {
+ value: 20
+ }
+ bottom {
+ value: 30
+ }
+ start {
+ value: 40
+ }
+ }
+ border {
+ color {
+ argb: 0xFFFF0000
+ }
+ width {
+ value: 5
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/box_with_corners_and_border_rtlaware.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/box_with_corners_and_border_rtlaware.textproto
new file mode 100644
index 0000000..390e626
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/box_with_corners_and_border_rtlaware.textproto
@@ -0,0 +1,46 @@
+box {
+ contents {
+ text {
+ text {
+ value: "Hello World"
+ }
+ }
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFF00FF00
+ }
+ corner {
+ radius {
+ value: 5
+ }
+ }
+ }
+ padding {
+ end {
+ value: 10
+ }
+ top {
+ value: 20
+ }
+ bottom {
+ value: 30
+ }
+ start {
+ value: 40
+ }
+ rtl_aware {
+ value: true
+ }
+ }
+ border {
+ color {
+ argb: 0xFFFF0000
+ }
+ width {
+ value: 5
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/box_with_fixed_size.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/box_with_fixed_size.textproto
new file mode 100644
index 0000000..bfaef0e
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/box_with_fixed_size.textproto
@@ -0,0 +1,35 @@
+box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ width {
+ expanded_dimension {}
+ }
+ height {
+ expanded_dimension {}
+ }
+ contents {
+ box {
+ width {
+ linear_dimension {
+ value: 50
+ }
+ }
+ height {
+ linear_dimension {
+ value: 50
+ }
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/broken_drawable.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/broken_drawable.textproto
new file mode 100644
index 0000000..a131617
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/broken_drawable.textproto
@@ -0,0 +1,46 @@
+# This is a bit of a weird Screenshot test; this won't have any output, but is needed
+# to ensure that the renderer doesn't crash with a broken image. Robolectric
+# does _not_ fail in the same way "real" Android does on invalid (or missing)
+# resources, so we're having to use layouts tests to test this case instead.
+#
+# Note the box is required though; the images should fail to inflate, so if this
+# layout only contained the image, then the renderer would just emit null and
+# the test harness would fail the test. Putting the box in ensures that the
+# renderer inflates _something_ to keep the rest of the harness happy.
+box {
+ contents {
+ image {
+ resource_id {
+ value: "broken_image"
+ }
+ width {
+ linear_dimension {
+ value: 16
+ }
+ }
+ height {
+ linear_dimension {
+ value: 16
+ }
+ }
+ }
+ }
+
+ contents {
+ image {
+ resource_id {
+ value: "missing_image"
+ }
+ width {
+ linear_dimension {
+ value: 16
+ }
+ }
+ height {
+ linear_dimension {
+ value: 16
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/column_with_alignment.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/column_with_alignment.textproto
new file mode 100644
index 0000000..1212653
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/column_with_alignment.textproto
@@ -0,0 +1,86 @@
+column {
+ contents {
+ column {
+ horizontal_alignment {
+ value: HALIGN_LEFT
+ }
+ contents {
+ text {
+ text {
+ value: "Align"
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "L"
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ column {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ contents {
+ text {
+ text {
+ value: "Align"
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "MID"
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ column {
+ horizontal_alignment {
+ value: HALIGN_RIGHT
+ }
+ contents {
+ text {
+ text {
+ value: "Align"
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "R"
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ column {
+ contents {
+ text {
+ text {
+ value: "Align undefined"
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "MID"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/column_with_alignment_rtlaware.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/column_with_alignment_rtlaware.textproto
new file mode 100644
index 0000000..1ba79d3
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/column_with_alignment_rtlaware.textproto
@@ -0,0 +1,86 @@
+column {
+ contents {
+ column {
+ horizontal_alignment {
+ value: HALIGN_START
+ }
+ contents {
+ text {
+ text {
+ value: "Align"
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "L"
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ column {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ contents {
+ text {
+ text {
+ value: "Align"
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "MID"
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ column {
+ horizontal_alignment {
+ value: HALIGN_END
+ }
+ contents {
+ text {
+ text {
+ value: "Align"
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "R"
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ column {
+ contents {
+ text {
+ text {
+ value: "Align undefined"
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "MID"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/column_with_height.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/column_with_height.textproto
new file mode 100644
index 0000000..8167667
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/column_with_height.textproto
@@ -0,0 +1,47 @@
+# This is a bit of a contrived example.
+# Add a column with minimal contents into a box, and align the box contents to
+# the bottom. If the column didn't have a height, it would just wrap its
+# contents and draw them at the bottom of the box. If the column has a height
+# then it will start placing elements from the top of the column, hence the
+# contents will be inset from the bottom of the parent box.
+box {
+ width {
+ expanded_dimension {}
+ }
+ height {
+ expanded_dimension {}
+ }
+ vertical_alignment {
+ value: VALIGN_BOTTOM
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ contents {
+ column {
+ height {
+ linear_dimension {
+ value: 100
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "Hi"
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "World"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/expanded_box_horizontal.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/expanded_box_horizontal.textproto
new file mode 100644
index 0000000..fce3d41
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/expanded_box_horizontal.textproto
@@ -0,0 +1,45 @@
+# Check that a box that is expanded does so correctly
+# Use a box at the top level.
+#
+# This is a pretty common paradigm; tiles will use boxes at the top level to
+# allow the positioning of multiple elements without them affecting the
+# placement of each other. If this is done incorrectly though, it can lead to
+# the inner box not expanding properly.
+box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ width {
+ expanded_dimension {}
+ }
+ height {
+ expanded_dimension {}
+ }
+ contents {
+ box {
+ horizontal_alignment {
+ value: HALIGN_START
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ width {
+ expanded_dimension {}
+ }
+ contents {
+ text {
+ text {
+ value: "Hello World"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/expanded_box_horizontal_right_align.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/expanded_box_horizontal_right_align.textproto
new file mode 100644
index 0000000..5ba5696
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/expanded_box_horizontal_right_align.textproto
@@ -0,0 +1,31 @@
+# Check that a box's width will be set even when its style is not set.
+# See b/165807783
+box {
+ width {
+ expanded_dimension {}
+ }
+ contents {
+ box {
+ horizontal_alignment {
+ value: HALIGN_END
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ width {
+ expanded_dimension {}
+ }
+ contents {
+ text {
+ text {
+ value: "Right-Aligned Text"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/expanded_box_vertical.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/expanded_box_vertical.textproto
new file mode 100644
index 0000000..44b4e02
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/expanded_box_vertical.textproto
@@ -0,0 +1,45 @@
+# Check that a box that is expanded does so correctly
+# Use a box at the top level.
+#
+# This is a pretty common paradigm; tiles will use boxes at the top level to
+# allow the positioning of multiple elements without them affecting the
+# placement of each other. If this is done incorrectly though, it can lead to
+# the inner box not expanding properly.
+box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ width {
+ expanded_dimension {}
+ }
+ height {
+ expanded_dimension {}
+ }
+ contents {
+ box {
+ vertical_alignment {
+ value: VALIGN_TOP
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ height {
+ expanded_dimension {}
+ }
+ contents {
+ text {
+ text {
+ value: "Hello World"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/expanded_children_in_row.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/expanded_children_in_row.textproto
new file mode 100644
index 0000000..aac4eaf
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/expanded_children_in_row.textproto
@@ -0,0 +1,178 @@
+column {
+ width {
+ expanded_dimension {}
+ }
+ height {
+ expanded_dimension {}
+ }
+
+ # A row that will not be displayed because it's set to wrap (by default), and
+ # all its children are set to expand, so it's undefined how to display it.
+ contents {
+ row {
+ contents {
+ box {
+ width {
+ expanded_dimension {}
+ }
+ height {
+ linear_dimension {
+ value: 20
+ }
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ box {
+ width {
+ expanded_dimension {}
+ }
+ height {
+ linear_dimension {
+ value: 20
+ }
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFF00FF00
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "Should not be displayed"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ spacer {
+ height {
+ linear_dimension {
+ value: 10
+ }
+ }
+ }
+ }
+
+ # A row with expand = true
+ contents {
+ row {
+ width {
+ expanded_dimension {}
+ }
+ contents {
+ box {
+ width {
+ expanded_dimension {}
+ }
+ height {
+ linear_dimension {
+ value: 20
+ }
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ box {
+ width {
+ expanded_dimension {}
+ }
+ height {
+ linear_dimension {
+ value: 20
+ }
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFF00FF00
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ spacer {
+ height {
+ linear_dimension {
+ value: 10
+ }
+ }
+ }
+ }
+
+ # A row with a known width
+ contents {
+ row {
+ width {
+ linear_dimension {
+ value: 100
+ }
+ }
+ contents {
+ box {
+ width {
+ expanded_dimension {}
+ }
+ height {
+ linear_dimension {
+ value: 20
+ }
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ }
+ }
+ contents {
+ box {
+ width {
+ expanded_dimension {}
+ }
+ height {
+ linear_dimension {
+ value: 20
+ }
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFF00FF00
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/font_weights_in_arc.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/font_weights_in_arc.textproto
new file mode 100644
index 0000000..e0d399f
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/font_weights_in_arc.textproto
@@ -0,0 +1,32 @@
+arc {
+ contents {
+ text {
+ font_style {
+ weight {
+ value: FONT_WEIGHT_NORMAL
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ text {
+ value: "Normal"
+ }
+ }
+ }
+ contents {
+ text {
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ text {
+ value: "Bold"
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/font_weights_in_spannable.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/font_weights_in_spannable.textproto
new file mode 100644
index 0000000..d5aa3bc
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/font_weights_in_spannable.textproto
@@ -0,0 +1,41 @@
+spannable {
+ max_lines {
+ value: 5
+ }
+ multiline_alignment {
+ value: HALIGN_START
+ }
+ line_spacing {
+ value: 20
+ }
+ spans {
+ text {
+ font_style {
+ weight {
+ value: FONT_WEIGHT_NORMAL
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ text {
+ value: "Normal "
+ }
+ }
+ }
+ spans {
+ text {
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ text {
+ value: "Bold"
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/image_expand_modes.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/image_expand_modes.textproto
new file mode 100644
index 0000000..53933eb
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/image_expand_modes.textproto
@@ -0,0 +1,128 @@
+column {
+ contents {
+ text {
+ text {
+ value: "Fit"
+ }
+ }
+ }
+ contents {
+ box {
+ modifiers {
+ border {
+ width {
+ value: 1
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ contents {
+ image {
+ width {
+ linear_dimension {
+ value: 48
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ resource_id {
+ value: "android_withbg_120dp"
+ }
+ content_scale_mode {
+ value: CONTENT_SCALE_MODE_FIT
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ text {
+ text {
+ value: "Crop"
+ }
+ }
+ }
+
+ contents {
+ box {
+ modifiers {
+ border {
+ width {
+ value: 1
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ contents {
+ image {
+ width {
+ linear_dimension {
+ value: 48
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ resource_id {
+ value: "android_withbg_120dp"
+ }
+ content_scale_mode {
+ value: CONTENT_SCALE_MODE_CROP
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ text {
+ text {
+ value: "FillBounds"
+ }
+ }
+ }
+ contents {
+ box {
+ modifiers {
+ border {
+ width {
+ value: 1
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ contents {
+ image {
+ width {
+ linear_dimension {
+ value: 48
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ resource_id {
+ value: "android_withbg_120dp"
+ }
+ content_scale_mode {
+ value: CONTENT_SCALE_MODE_FILL_BOUNDS
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/image_expanded_to_parent.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/image_expanded_to_parent.textproto
new file mode 100644
index 0000000..3cc85e7
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/image_expanded_to_parent.textproto
@@ -0,0 +1,28 @@
+box {
+ width {
+ expanded_dimension {}
+ }
+ height {
+ expanded_dimension {}
+ }
+ modifiers {
+ padding {
+ top {
+ value: 50
+ }
+ }
+ }
+ contents {
+ image {
+ resource_id {
+ value: "android_withbg_120dp"
+ }
+ width {
+ expanded_dimension {}
+ }
+ height {
+ expanded_dimension {}
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/image_oversized_in_box.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/image_oversized_in_box.textproto
new file mode 100644
index 0000000..2bbb125
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/image_oversized_in_box.textproto
@@ -0,0 +1,142 @@
+column {
+ width {
+ expanded_dimension {}
+ }
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ contents {
+ box {
+ modifiers {
+ border {
+ width {
+ value: 1
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ horizontal_alignment {
+ value: HALIGN_START
+ }
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 48
+ }
+ }
+ contents {
+ image {
+ resource_id {
+ value: "android_withbg_120dp"
+ }
+ width {
+ linear_dimension {
+ value: 48
+ }
+ }
+ height {
+ linear_dimension {
+ value: 48
+ }
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ box {
+ modifiers {
+ border {
+ width {
+ value: 1
+ }
+ color {
+ argb: 0xFFFFFF00
+ }
+ }
+ }
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 48
+ }
+ }
+ contents {
+ image {
+ resource_id {
+ value: "android_withbg_120dp"
+ }
+ width {
+ linear_dimension {
+ value: 48
+ }
+ }
+ height {
+ linear_dimension {
+ value: 48
+ }
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ box {
+ modifiers {
+ border {
+ width {
+ value: 1
+ }
+ color {
+ argb: 0xFF0000FF
+ }
+ }
+ }
+ horizontal_alignment {
+ value: HALIGN_END
+ }
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 48
+ }
+ }
+ contents {
+ image {
+ resource_id {
+ value: "android_withbg_120dp"
+ }
+ width {
+ linear_dimension {
+ value: 48
+ }
+ }
+ height {
+ linear_dimension {
+ value: 48
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/image_oversized_in_box_proportional.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/image_oversized_in_box_proportional.textproto
new file mode 100644
index 0000000..e84fb02
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/image_oversized_in_box_proportional.textproto
@@ -0,0 +1,145 @@
+column {
+ width {
+ expanded_dimension {}
+ }
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ contents {
+ box {
+ modifiers {
+ border {
+ width {
+ value: 1
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ horizontal_alignment {
+ value: HALIGN_START
+ }
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 48
+ }
+ }
+ contents {
+ image {
+ resource_id {
+ value: "android_withbg_120dp"
+ }
+ width {
+ proportional_dimension {
+ aspect_ratio_width: 1
+ aspect_ratio_height: 1
+ }
+ }
+ height {
+ linear_dimension {
+ value: 48
+ }
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ box {
+ modifiers {
+ border {
+ width {
+ value: 1
+ }
+ color {
+ argb: 0xFFFFFF00
+ }
+ }
+ }
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 48
+ }
+ }
+ contents {
+ image {
+ resource_id {
+ value: "android_withbg_120dp"
+ }
+ width {
+ proportional_dimension {
+ aspect_ratio_width: 1
+ aspect_ratio_height: 1
+ }
+ }
+ height {
+ linear_dimension {
+ value: 48
+ }
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ box {
+ modifiers {
+ border {
+ width {
+ value: 1
+ }
+ color {
+ argb: 0xFF0000FF
+ }
+ }
+ }
+ horizontal_alignment {
+ value: HALIGN_END
+ }
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 48
+ }
+ }
+ contents {
+ image {
+ resource_id {
+ value: "android_withbg_120dp"
+ }
+ width {
+ proportional_dimension {
+ aspect_ratio_width: 1
+ aspect_ratio_height: 1
+ }
+ }
+ height {
+ linear_dimension {
+ value: 48
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/image_proportional_resize.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/image_proportional_resize.textproto
new file mode 100644
index 0000000..394d1f3
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/image_proportional_resize.textproto
@@ -0,0 +1,30 @@
+box {
+ # Add a green background, so we can tell whether the image "box" is resizing
+ # properly.
+ modifiers {
+ background {
+ color {
+ argb: 0xFF00FF00
+ }
+ }
+ }
+ width {
+ expanded_dimension {}
+ }
+ contents {
+ image {
+ width {
+ expanded_dimension {}
+ }
+ height {
+ proportional_dimension {
+ aspect_ratio_width: 2
+ aspect_ratio_height: 1
+ }
+ }
+ resource_id {
+ value: "android"
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/image_with_dimensions.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/image_with_dimensions.textproto
new file mode 100644
index 0000000..d017f08
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/image_with_dimensions.textproto
@@ -0,0 +1,15 @@
+image {
+ resource_id {
+ value: "android"
+ }
+ width {
+ linear_dimension {
+ value: 20
+ }
+ }
+ height {
+ linear_dimension {
+ value: 15
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/image_with_inline_data.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/image_with_inline_data.textproto
new file mode 100644
index 0000000..1daac1c
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/image_with_inline_data.textproto
@@ -0,0 +1,15 @@
+image {
+ resource_id {
+ value: "inline"
+ }
+ width {
+ linear_dimension {
+ value: 16
+ }
+ }
+ height {
+ linear_dimension {
+ value: 16
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/image_with_padding.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/image_with_padding.textproto
new file mode 100644
index 0000000..f201acb
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/image_with_padding.textproto
@@ -0,0 +1,44 @@
+image {
+ resource_id {
+ value: "android"
+ }
+ width {
+ linear_dimension {
+ value: 52
+ }
+ }
+ height {
+ linear_dimension {
+ value: 52
+ }
+ }
+ content_scale_mode {
+ value: CONTENT_SCALE_MODE_FILL_BOUNDS
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ corner {
+ radius {
+ value: 100
+ }
+ }
+ }
+ padding {
+ start {
+ value: 16
+ }
+ end {
+ value: 16
+ }
+ top {
+ value: 16
+ }
+ bottom {
+ value: 16
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/line_in_arc.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/line_in_arc.textproto
new file mode 100644
index 0000000..60d5d53
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/line_in_arc.textproto
@@ -0,0 +1,21 @@
+arc {
+ anchor_angle: {
+ value: 0
+ }
+ anchor_type: {
+ value: ARC_ANCHOR_START
+ }
+ contents {
+ line {
+ length {
+ value: 100
+ }
+ thickness {
+ value: 10
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/line_multi_height.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/line_multi_height.textproto
new file mode 100644
index 0000000..2d0df13
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/line_multi_height.textproto
@@ -0,0 +1,269 @@
+column {
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 100
+ }
+ }
+ height {
+ linear_dimension {
+ value: 10
+ }
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ corner {
+ radius {
+ value: 5
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 100
+ }
+ }
+ height {
+ linear_dimension {
+ value: 20
+ }
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFF00FF00
+ }
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 100
+ }
+ }
+ height {
+ linear_dimension {
+ value: 40
+ }
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFFFF00
+ }
+ corner {
+ radius {
+ value: 20
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 100
+ }
+ }
+ height {
+ linear_dimension {
+ value: 50
+ }
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFF00FFFF
+ }
+ corner {
+ radius {
+ value: 25
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ box {
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFFFF00
+ }
+ }
+ }
+ contents {
+ row {
+ contents {
+ text {
+ text {
+ value: "0-length line ->"
+ }
+ font_style {
+ size {
+ value: 14
+ }
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ color {
+ argb: 0xFF000000
+ }
+ }
+ }
+ }
+ contents {
+ # Spacer with no length but a thickness of 40dp.
+ spacer {
+ height {
+ linear_dimension {
+ value: 40
+ }
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFF000000
+ }
+ corner {
+ radius {
+ value: 20
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "<-"
+ }
+ font_style {
+ size {
+ value: 14
+ }
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ color {
+ argb: 0xFF000000
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ box {
+ modifiers {
+ background {
+ color {
+ argb: 0xFF00FF00
+ }
+ }
+ }
+ contents {
+ column {
+ horizontal_alignment {
+ value: HALIGN_START
+ }
+ contents {
+ text {
+ text {
+ value: "v 0-thickness line"
+ }
+ font_style {
+ size {
+ value: 14
+ }
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ color {
+ argb: 0xFF000000
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ # Spacer with no thickness but a length of 160dp.
+ width {
+ linear_dimension {
+ value: 160
+ }
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFF000000
+ }
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "^"
+ }
+ font_style {
+ size {
+ value: 14
+ }
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ color {
+ argb: 0xFF000000
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ box {
+ modifiers {
+ background {
+ color {
+ argb: 0xFF0000FF
+ }
+ }
+ }
+ contents {
+ spacer {
+ # Spacer with no dimensions. Neither the spacer nor the blue container
+ # around it should be visible.
+ modifiers {
+ background {
+ color {
+ argb: 0xFF000000
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/long_text.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/long_text.textproto
new file mode 100644
index 0000000..9bb03b7
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/long_text.textproto
@@ -0,0 +1,84 @@
+box {
+ width {
+ linear_dimension {
+ value: 100
+ }
+ }
+ height {
+ wrapped_dimension {}
+ }
+
+ # Add a background color so it's obvious what the bounds of the text is.
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFFFFFF
+ }
+ }
+ }
+
+ contents {
+ column {
+ contents {
+ text {
+ text {
+ value: "This is a really long string which really should be truncated when it's rendered in a really little container."
+ }
+ font_style {
+ color {
+ argb: 0xFF00FF00
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "This is a really long string which really should be truncated when it's rendered in a really little container."
+ }
+ overflow {
+ value: TEXT_OVERFLOW_TRUNCATE
+ }
+ font_style {
+ color {
+ argb: 0xFF000000
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "This is a really long string which really should be ellipsized when it's rendered in a really little container."
+ }
+ overflow {
+ value: TEXT_OVERFLOW_ELLIPSIZE_END
+ }
+ font_style {
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "This is a really long string which really should be truncated when it's rendered in a really little container."
+ }
+ max_lines: {
+ value: 2
+ }
+ overflow {
+ value: TEXT_OVERFLOW_ELLIPSIZE_END
+ }
+ font_style {
+ color {
+ argb: 0xFF0000FF
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/multi_line_text_alignment.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/multi_line_text_alignment.textproto
new file mode 100644
index 0000000..1c3bc0d
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/multi_line_text_alignment.textproto
@@ -0,0 +1,111 @@
+column {
+ contents {
+ spannable {
+ multiline_alignment {
+ value: HALIGN_START
+ }
+ max_lines: {
+ value: 5
+ }
+ spans {
+ text {
+ text {
+ value: "This should be a multi-line bit of text that should be left-aligned"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ spannable {
+ multiline_alignment {
+ value: HALIGN_CENTER
+ }
+ max_lines: {
+ value: 5
+ }
+ spans {
+ text {
+ text {
+ value: "This should be a multi-line bit of text that should be center-aligned"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFF00FF00
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ spannable {
+ multiline_alignment {
+ value: HALIGN_END
+ }
+ max_lines: {
+ value: 5
+ }
+ spans {
+ text {
+ text {
+ value: "This should be a multi-line bit of text that should be right-aligned"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFF0000FF
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ spannable {
+ max_lines: {
+ value: 5
+ }
+ spans {
+ text {
+ text {
+ value: "Multi-line with no alignment. Should be center-aligned"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/row_column_space_test.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/row_column_space_test.textproto
new file mode 100644
index 0000000..3bf6e9e
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/row_column_space_test.textproto
@@ -0,0 +1,230 @@
+row {
+ contents {
+ column {
+ contents {
+ image {
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ resource_id {
+ value: "android"
+ }
+ width {
+ linear_dimension {
+ value: 20
+ }
+ }
+ height {
+ linear_dimension {
+ value: 15
+ }
+ }
+ }
+ }
+ contents {
+ image {
+ modifiers {
+ background {
+ color {
+ argb: 0xFF00FF00
+ }
+ }
+ }
+ resource_id {
+ value: "android"
+ }
+ width {
+ linear_dimension {
+ value: 20
+ }
+ }
+ height {
+ linear_dimension {
+ value: 15
+ }
+ }
+ }
+ }
+ contents {
+ image {
+ modifiers {
+ background {
+ color {
+ argb: 0xFF0000FF
+ }
+ }
+ }
+ resource_id {
+ value: "android"
+ }
+ width {
+ linear_dimension {
+ value: 20
+ }
+ }
+ height {
+ linear_dimension {
+ value: 15
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ column {
+ contents {
+ image {
+ modifiers {
+ background {
+ color {
+ argb: 0xFF00FF00
+ }
+ }
+ }
+ resource_id {
+ value: "android"
+ }
+ width {
+ linear_dimension {
+ value: 20
+ }
+ }
+ height {
+ linear_dimension {
+ value: 15
+ }
+ }
+ }
+ }
+ contents {
+ image {
+ modifiers {
+ background {
+ color {
+ argb: 0xFF0000FF
+ }
+ }
+ }
+ resource_id {
+ value: "android"
+ }
+ width {
+ linear_dimension {
+ value: 20
+ }
+ }
+ height {
+ linear_dimension {
+ value: 15
+ }
+ }
+ }
+ }
+ contents {
+ image {
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ resource_id {
+ value: "android"
+ }
+ width {
+ linear_dimension {
+ value: 20
+ }
+ }
+ height {
+ linear_dimension {
+ value: 15
+ }
+ }
+ }
+ }
+ }
+ }
+ contents {
+ column {
+ contents {
+ image {
+ modifiers {
+ background {
+ color {
+ argb: 0xFF0000FF
+ }
+ }
+ }
+ resource_id {
+ value: "android"
+ }
+ width {
+ linear_dimension {
+ value: 20
+ }
+ }
+ height {
+ linear_dimension {
+ value: 15
+ }
+ }
+ }
+ }
+ contents {
+ image {
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ resource_id {
+ value: "android"
+ }
+ width {
+ linear_dimension {
+ value: 20
+ }
+ }
+ height {
+ linear_dimension {
+ value: 15
+ }
+ }
+ }
+ }
+ contents {
+ image {
+ modifiers {
+ background {
+ color {
+ argb: 0xFF00FF00
+ }
+ }
+ }
+ resource_id {
+ value: "android"
+ }
+ width {
+ linear_dimension {
+ value: 20
+ }
+ }
+ height {
+ linear_dimension {
+ value: 15
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/row_with_alignment.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/row_with_alignment.textproto
new file mode 100644
index 0000000..777566b
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/row_with_alignment.textproto
@@ -0,0 +1,126 @@
+column {
+ contents {
+ row {
+ vertical_alignment {
+ value: VALIGN_TOP
+ }
+ contents {
+ image {
+ resource_id {
+ value: "android"
+ }
+ width {
+ linear_dimension {
+ value: 45
+ }
+ }
+ height {
+ linear_dimension {
+ value: 45
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "T"
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ row {
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ contents {
+ image {
+ resource_id {
+ value: "android"
+ }
+ width {
+ linear_dimension {
+ value: 45
+ }
+ }
+ height {
+ linear_dimension {
+ value: 45
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "MID"
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ row {
+ vertical_alignment {
+ value: VALIGN_BOTTOM
+ }
+ contents {
+ image {
+ resource_id {
+ value: "android"
+ }
+ width {
+ linear_dimension {
+ value: 45
+ }
+ }
+ height {
+ linear_dimension {
+ value: 45
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "B"
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ row {
+ contents {
+ image {
+ resource_id {
+ value: "android"
+ }
+ width {
+ linear_dimension {
+ value: 45
+ }
+ }
+ height {
+ linear_dimension {
+ value: 45
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "Default (MID)"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/row_with_width.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/row_with_width.textproto
new file mode 100644
index 0000000..626b2a5
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/row_with_width.textproto
@@ -0,0 +1,47 @@
+# This is a bit of a contrived example.
+# Add a row with minimal contents into a box, and align the box contents to the
+# right. If the row didn't have a width, it would just wrap its contents and
+# draw them at the right of the box. If the row has a width then it will start
+# placing elements from the left of the row, hence the contents will be inset
+# from the right hand side of the parent box.
+box {
+ width {
+ expanded_dimension {}
+ }
+ height {
+ expanded_dimension {}
+ }
+ horizontal_alignment {
+ value: HALIGN_END
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ contents {
+ row {
+ width {
+ linear_dimension {
+ value: 100
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "Hi"
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "World"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/simple_text.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/simple_text.textproto
new file mode 100644
index 0000000..4cd98d5
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/simple_text.textproto
@@ -0,0 +1,5 @@
+text {
+ text {
+ value: "Hello World"
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/single_line_text_alignment.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/single_line_text_alignment.textproto
new file mode 100644
index 0000000..c30f051
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/single_line_text_alignment.textproto
@@ -0,0 +1,65 @@
+column {
+ contents {
+ text {
+ text {
+ value: "Single-line text"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ multiline_alignment {
+ value: TEXT_ALIGN_START
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "Single-line text"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFF00FF00
+ }
+ }
+ multiline_alignment {
+ value: TEXT_ALIGN_CENTER
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "Single-line text"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFF0000FF
+ }
+ }
+ multiline_alignment {
+ value: TEXT_ALIGN_END
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/spacer_horizontal.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/spacer_horizontal.textproto
new file mode 100644
index 0000000..fa8dc2c
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/spacer_horizontal.textproto
@@ -0,0 +1,89 @@
+row {
+ contents {
+ text {
+ text {
+ value: "This"
+ }
+ font_style {
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ max_lines: {
+ value: 1000
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 10
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "Is"
+ }
+ font_style {
+ color {
+ argb: 0xFF00FF00
+ }
+ }
+ max_lines: {
+ value: 1000
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 20
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "A"
+ }
+ font_style {
+ color {
+ argb: 0xFF0000FF
+ }
+ }
+ max_lines: {
+ value: 1000
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 30
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "Test"
+ }
+ font_style {
+ color {
+ argb: 0xFF00FFFF
+ }
+ }
+ max_lines: {
+ value: 1000
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/spacer_in_arc.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/spacer_in_arc.textproto
new file mode 100644
index 0000000..b4477b7
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/spacer_in_arc.textproto
@@ -0,0 +1,219 @@
+arc {
+ anchor_angle: {
+ value: 0
+ }
+ anchor_type: {
+ value: ARC_ANCHOR_START
+ }
+
+ contents {
+ spacer {
+ length {
+ value: 90
+ }
+ }
+ }
+
+ contents {
+ adapter {
+ rotate_contents {
+ value: true
+ }
+ content {
+ box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFFFF0000
+ }
+ corner {
+ radius {
+ value: 12
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "1"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFFFFFF
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ spacer {
+ length {
+ value: 10
+ }
+ }
+ }
+
+ contents {
+ adapter {
+ rotate_contents {
+ value: true
+ }
+ content {
+ box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFF00FF00
+ }
+ corner {
+ radius {
+ value: 12
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "2"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFFFFFF
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ contents {
+ spacer {
+ length {
+ value: 30
+ }
+ }
+ }
+
+ contents {
+ adapter {
+ rotate_contents {
+ value: true
+ }
+ content {
+ box {
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xFF0000FF
+ }
+ corner {
+ radius {
+ value: 12
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 24
+ }
+ }
+ height {
+ linear_dimension {
+ value: 24
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "3"
+ }
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ size {
+ value: 16
+ }
+ color {
+ argb: 0xFFFFFFFF
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/spacer_vertical.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/spacer_vertical.textproto
new file mode 100644
index 0000000..c196cc8
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/spacer_vertical.textproto
@@ -0,0 +1,65 @@
+column {
+ contents {
+ text {
+ text {
+ value: "Hello World"
+ }
+ font_style {
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ max_lines: {
+ value: 1000
+ }
+ }
+ }
+ contents {
+ spacer {
+ height {
+ linear_dimension {
+ value: 10
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "More text"
+ }
+ font_style {
+ color {
+ argb: 0xFF00FF00
+ }
+ }
+ max_lines: {
+ value: 1000
+ }
+ }
+ }
+ contents {
+ spacer {
+ height {
+ linear_dimension {
+ value: 30
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "Yet more text"
+ }
+ font_style {
+ color {
+ argb: 0xFF0000FF
+ }
+ }
+ max_lines: {
+ value: 1000
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/spannable_image.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/spannable_image.textproto
new file mode 100644
index 0000000..bdafd00
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/spannable_image.textproto
@@ -0,0 +1,45 @@
+spannable {
+ max_lines {
+ value: 5
+ }
+ multiline_alignment {
+ value: HALIGN_START
+ }
+ spans {
+ text {
+ font_style {
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ text {
+ value: "Hello"
+ }
+ }
+ }
+ spans {
+ image {
+ resource_id {
+ value: "android"
+ }
+ width {
+ value: 24
+ }
+ height {
+ value: 24
+ }
+ }
+ }
+ spans {
+ text {
+ font_style {
+ color {
+ argb: 0xFF00FF00
+ }
+ }
+ text {
+ value: "World"
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/spannable_image_with_clickable.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/spannable_image_with_clickable.textproto
new file mode 100644
index 0000000..4c53354
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/spannable_image_with_clickable.textproto
@@ -0,0 +1,53 @@
+spannable {
+ max_lines {
+ value: 5
+ }
+ multiline_alignment {
+ value: HALIGN_START
+ }
+ spans {
+ text {
+ font_style {
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ text {
+ value: "Hello"
+ }
+ }
+ }
+ spans {
+ image {
+ resource_id {
+ value: "android"
+ }
+ width {
+ value: 24
+ }
+ height {
+ value: 24
+ }
+ modifiers {
+ clickable {
+ id: "HelloWorld"
+ on_click {
+ load_action {}
+ }
+ }
+ }
+ }
+ }
+ spans {
+ text {
+ font_style {
+ color {
+ argb: 0xFF00FF00
+ }
+ }
+ text {
+ value: "World"
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/spannable_image_wrapped.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/spannable_image_wrapped.textproto
new file mode 100644
index 0000000..e408122
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/spannable_image_wrapped.textproto
@@ -0,0 +1,52 @@
+column {
+ contents {
+ spannable {
+ max_lines {
+ value: 5
+ }
+ multiline_alignment {
+ value: HALIGN_START
+ }
+ spans {
+ text {
+ font_style {
+ color {
+ argb: 0xFFFF0000
+ }
+ size {
+ value: 32
+ }
+ }
+ text {
+ value: "abcdefghijk"
+ }
+ }
+ }
+ spans {
+ image {
+ resource_id {
+ value: "android"
+ }
+ width {
+ value: 24
+ }
+ height {
+ value: 24
+ }
+ }
+ }
+ spans {
+ text {
+ font_style {
+ color {
+ argb: 0xFF00FF00
+ }
+ }
+ text {
+ value: "abcdefg"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/spannable_text.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/spannable_text.textproto
new file mode 100644
index 0000000..aba61e0
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/spannable_text.textproto
@@ -0,0 +1,124 @@
+column {
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 150
+ }
+ }
+ height {
+ linear_dimension {
+ value: 2
+ }
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xfffcba03
+ }
+ }
+ }
+ }
+ }
+ contents {
+ spannable {
+ max_lines {
+ value: 5
+ }
+ multiline_alignment {
+ value: HALIGN_START
+ }
+ line_spacing {
+ value: 20
+ }
+ spans {
+ text {
+ font_style {
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ text {
+ value: "Hello World "
+ }
+ }
+ }
+ spans {
+ text {
+ font_style {
+ weight {
+ value: FONT_WEIGHT_BOLD
+ }
+ color {
+ argb: 0xFF00FF00
+ }
+ }
+ text {
+ value: "This is a test that should wrap "
+ }
+ }
+ }
+ spans {
+ text {
+ font_style {
+ italic {
+ value: true
+ }
+ underline {
+ value: true
+ }
+ size {
+ value: 24
+ }
+ color {
+ argb: 0xFF0000FF
+ }
+ }
+ text {
+ value: "across multiple lines!"
+ }
+ }
+ }
+ spans {
+ text {
+ font_style {
+ size {
+ value: 14
+ }
+ color {
+ argb: 0xFF4287f5
+ }
+ letter_spacing {
+ value: 0.6
+ }
+ }
+ text {
+ value: "Social distancing! "
+ }
+ }
+ }
+ }
+ }
+ contents {
+ spacer {
+ width {
+ linear_dimension {
+ value: 150
+ }
+ }
+ height {
+ linear_dimension {
+ value: 2
+ }
+ }
+ modifiers {
+ background {
+ color {
+ argb: 0xfffcba03
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/text_and_image_in_box.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/text_and_image_in_box.textproto
new file mode 100644
index 0000000..e0d791d
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/text_and_image_in_box.textproto
@@ -0,0 +1,29 @@
+# A box which contains multiple children should just layer them on top of each
+# other.
+box {
+ contents {
+ image {
+ resource_id {
+ value: "android"
+ }
+ width {
+ linear_dimension {
+ value: 18
+ }
+ }
+ height {
+ linear_dimension {
+ value: 18
+ }
+ }
+ }
+ }
+
+ contents {
+ text {
+ text {
+ value: "Hello World"
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/text_default_size.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/text_default_size.textproto
new file mode 100644
index 0000000..7ecd823
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/text_default_size.textproto
@@ -0,0 +1,34 @@
+# Expect the default text size to be the same regardless of whether or not it
+# has a color style. See b/165807784
+box {
+ modifiers {
+ background {
+ color {
+ argb: 0xFF000000
+ }
+ }
+ }
+ contents {
+ column {
+ contents {
+ text {
+ text {
+ value: "Default Size (No Style)"
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "Default Size (With Style)"
+ }
+ font_style {
+ color {
+ argb: 0xFFFFBE00
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/text_in_column.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/text_in_column.textproto
new file mode 100644
index 0000000..ad412e1
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/text_in_column.textproto
@@ -0,0 +1,38 @@
+column {
+ contents {
+ text {
+ text {
+ value: "Hello World"
+ }
+ font_style {
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "More text"
+ }
+ font_style {
+ color {
+ argb: 0xFF00FF00
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "Yet more text"
+ }
+ font_style {
+ color {
+ argb: 0xFF0000FF
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/text_in_row.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/text_in_row.textproto
new file mode 100644
index 0000000..0b20258
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/text_in_row.textproto
@@ -0,0 +1,50 @@
+row {
+ contents {
+ text {
+ text {
+ value: "This"
+ }
+ font_style {
+ color {
+ argb: 0xFFFF0000
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "Is"
+ }
+ font_style {
+ color {
+ argb: 0xFF00FF00
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "A"
+ }
+ font_style {
+ color {
+ argb: 0xFF0000FF
+ }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "Test"
+ }
+ font_style {
+ color {
+ argb: 0xFF00FFFF
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/text_with_font_weights.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/text_with_font_weights.textproto
new file mode 100644
index 0000000..ded6fd2
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/text_with_font_weights.textproto
@@ -0,0 +1,71 @@
+box {
+ modifiers {
+ background {
+ color {
+ argb: 0xFF000000
+ }
+ }
+ }
+ width {
+ expanded_dimension: {}
+ }
+ height {
+ expanded_dimension: {}
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ contents {
+ column {
+ horizontal_alignment {
+ value: HALIGN_LEFT
+ }
+ contents {
+ text {
+ text {
+ value: "Weight=Unset (Normal)"
+ }
+ font_style {
+ size { value: 14 }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "Weight=Undefined (Normal)"
+ }
+ font_style {
+ size { value: 14 }
+ weight { value: FONT_WEIGHT_UNDEFINED }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "Weight=400 (Normal)"
+ }
+ font_style {
+ size { value: 14 }
+ weight { value: FONT_WEIGHT_NORMAL }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "Weight=700 (Bold)"
+ }
+ font_style {
+ size { value: 14 }
+ weight { value: FONT_WEIGHT_BOLD }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/text_with_font_weights_italic.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/text_with_font_weights_italic.textproto
new file mode 100644
index 0000000..34cc5a5
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/text_with_font_weights_italic.textproto
@@ -0,0 +1,75 @@
+box {
+ modifiers {
+ background {
+ color {
+ argb: 0xFF000000
+ }
+ }
+ }
+ width {
+ expanded_dimension: {}
+ }
+ height {
+ expanded_dimension: {}
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ contents {
+ column {
+ horizontal_alignment {
+ value: HALIGN_LEFT
+ }
+ contents {
+ text {
+ text {
+ value: "Weight=Unset (Normal, Italic)"
+ }
+ font_style {
+ size { value: 14 }
+ italic { value: true }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "Weight=Undef (Normal, Italic)"
+ }
+ font_style {
+ size { value: 14 }
+ weight { value: FONT_WEIGHT_UNDEFINED }
+ italic { value: true }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "Weight=400 (Normal, Italic)"
+ }
+ font_style {
+ size { value: 14 }
+ weight { value: FONT_WEIGHT_NORMAL }
+ italic { value: true }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "Weight=700 (Bold, Italic)"
+ }
+ font_style {
+ size { value: 14 }
+ weight { value: FONT_WEIGHT_BOLD }
+ italic { value: true }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/text_with_spacing.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/text_with_spacing.textproto
new file mode 100644
index 0000000..9e4a193
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/text_with_spacing.textproto
@@ -0,0 +1,73 @@
+box {
+ modifiers {
+ background {
+ color {
+ argb: 0xFF000000
+ }
+ }
+ }
+ width {
+ expanded_dimension: {}
+ }
+ height {
+ expanded_dimension: {}
+ }
+ vertical_alignment {
+ value: VALIGN_CENTER
+ }
+ horizontal_alignment {
+ value: HALIGN_CENTER
+ }
+ contents {
+ column {
+ horizontal_alignment {
+ value: HALIGN_LEFT
+ }
+ contents {
+ text {
+ text {
+ value: "Normal height"
+ }
+ font_style {
+ size { value: 14 }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "size 14sp, lineHeight 34sp. This should be a longer text to show the baseline distance."
+ }
+ max_lines { value: 10 }
+ line_height { value: 34 }
+ font_style {
+ size { value: 14 }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "size 14sp, letterSpacing 0.5em"
+ }
+ max_lines { value: 10 }
+ font_style {
+ size { value: 14 }
+ letter_spacing { value: 0.5 }
+ }
+ }
+ }
+ contents {
+ text {
+ text {
+ value: "size 14sp, letterSpacing -0.1em"
+ }
+ font_style {
+ size { value: 14 }
+ letter_spacing { value: -0.1 }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/raw/text_with_style_no_color.textproto b/wear/wear-tiles-renderer/src/androidTest/res/raw/text_with_style_no_color.textproto
new file mode 100644
index 0000000..8206ef9
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/raw/text_with_style_no_color.textproto
@@ -0,0 +1,13 @@
+spannable {
+ multiline_alignment {
+ value: HALIGN_START
+ }
+ spans {
+ text {
+ text {
+ value: "Hello World"
+ }
+ }
+ }
+}
+
diff --git a/wear/wear-tiles-renderer/src/androidTest/res/values/ic_channel_background.xml b/wear/wear-tiles-renderer/src/androidTest/res/values/ic_channel_background.xml
new file mode 100644
index 0000000..fe558ce
--- /dev/null
+++ b/wear/wear-tiles-renderer/src/androidTest/res/values/ic_channel_background.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="ic_channel_background">#3DDC84</color>
+</resources>
diff --git a/wear/wear-tiles-renderer/src/main/java/androidx/wear/tiles/renderer/ResourceAccessors.java b/wear/wear-tiles-renderer/src/main/java/androidx/wear/tiles/renderer/ResourceAccessors.java
index 5bc9bd6..4a297d3 100644
--- a/wear/wear-tiles-renderer/src/main/java/androidx/wear/tiles/renderer/ResourceAccessors.java
+++ b/wear/wear-tiles-renderer/src/main/java/androidx/wear/tiles/renderer/ResourceAccessors.java
@@ -122,7 +122,7 @@
this.mProtoResources = protoResources;
}
- /** Set the resource loader for {@link AndroidImageResourceByResId} resources. */
+ /** Set the resource loader for {@link AndroidImageResourceByResIdAccessor} resources. */
@NonNull
@SuppressLint("MissingGetterMatchingBuilder")
public Builder setAndroidImageResourceByResIdAccessor(
diff --git a/wear/wear-tiles/src/main/java/androidx/wear/tiles/TileProviderService.java b/wear/wear-tiles/src/main/java/androidx/wear/tiles/TileProviderService.java
index ab51044..10dfabc 100644
--- a/wear/wear-tiles/src/main/java/androidx/wear/tiles/TileProviderService.java
+++ b/wear/wear-tiles/src/main/java/androidx/wear/tiles/TileProviderService.java
@@ -82,7 +82,7 @@
* Called when the system is requesting a new timeline from this Tile Provider. Note that this
* may be called from a background thread.
*
- * @param requestParams Parameters about the request. See {@link TileRequestData} for more info.
+ * @param requestParams Parameters about the request. See {@link TileRequest} for more info.
*/
@MainThread
@NonNull
@@ -92,8 +92,8 @@
* Called when the system is requesting a resource bundle from this Tile Provider. Note that
* this may be called from a background thread.
*
- * @param requestParams Parameters about the request. See {@link ResourcesRequestData} for more
- * info.
+ * @param requestParams Parameters about the request. See {@link ResourcesRequest} for more
+ * info.
*/
@MainThread
@NonNull
@@ -104,7 +104,7 @@
* Called when a tile provided by this Tile Provider is added to the carousel. Note that this
* may be called from a background thread.
*
- * @param requestParams Parameters about the request. See {@link TileAddEventData} for more
+ * @param requestParams Parameters about the request. See {@link TileAddEvent} for more
* info.
*/
@MainThread
@@ -114,7 +114,7 @@
* Called when a tile provided by this Tile Provider is removed from the carousel. Note that
* this may be called from a background thread.
*
- * @param requestParams Parameters about the request. See {@link TileRemoveEventData} for more
+ * @param requestParams Parameters about the request. See {@link TileRemoveEvent} for more
* info.
*/
@MainThread
@@ -124,7 +124,7 @@
* Called when a tile provided by this Tile Provider becomes into view, on screen. Note that
* this may be called from a background thread.
*
- * @param requestParams Parameters about the request. See {@link TileEnterEventData} for more
+ * @param requestParams Parameters about the request. See {@link TileEnterEvent} for more
* info.
*/
@MainThread
@@ -134,7 +134,7 @@
* Called when a tile provided by this Tile Provider goes out of view, on screen. Note that this
* may be called from a background thread.
*
- * @param requestParams Parameters about the request. See {@link TileLeaveEventData} for more
+ * @param requestParams Parameters about the request. See {@link TileLeaveEvent} for more
* info.
*/
@MainThread
diff --git a/wear/wear-tiles/src/main/java/androidx/wear/tiles/builders/LayoutElementBuilders.java b/wear/wear-tiles/src/main/java/androidx/wear/tiles/builders/LayoutElementBuilders.java
index eea253a..3f1f665 100644
--- a/wear/wear-tiles/src/main/java/androidx/wear/tiles/builders/LayoutElementBuilders.java
+++ b/wear/wear-tiles/src/main/java/androidx/wear/tiles/builders/LayoutElementBuilders.java
@@ -1812,8 +1812,7 @@
}
/**
- * Sets how to align the contents of this container relative to anchor_angle. See the
- * descriptions of options in {@link ArcAnchorType} for more information. If not
+ * Sets how to align the contents of this container relative to anchor_angle. If not
* defined, defaults to ARC_ANCHOR_CENTER.
*/
@SuppressLint("MissingGetterMatchingBuilder")
diff --git a/wear/wear-tiles/src/main/java/androidx/wear/tiles/readers/EventReaders.java b/wear/wear-tiles/src/main/java/androidx/wear/tiles/readers/EventReaders.java
index 8f2324e..2ae5a0a 100644
--- a/wear/wear-tiles/src/main/java/androidx/wear/tiles/readers/EventReaders.java
+++ b/wear/wear-tiles/src/main/java/androidx/wear/tiles/readers/EventReaders.java
@@ -31,7 +31,7 @@
public class EventReaders {
private EventReaders() {}
- /** Reader for a {@link TileAddEventData} instance. */
+ /** Reader for Tile add event parameters. */
public static class TileAddEvent {
private final EventProto.TileAddEvent mProto;
@@ -64,7 +64,7 @@
}
}
- /** Reader for a {@link TileRemoveEventData} instance. */
+ /** Reader for Tile remove event parameters. */
public static class TileRemoveEvent {
private final EventProto.TileRemoveEvent mProto;
@@ -97,7 +97,7 @@
}
}
- /** Reader for a {@link TileEnterEventData} instance. */
+ /** Reader for Tile enter event parameters. */
public static class TileEnterEvent {
private final EventProto.TileEnterEvent mProto;
@@ -130,7 +130,7 @@
}
}
- /** Reader for a {@link TileLeaveEventData} instance. */
+ /** Reader for a Tile leave event parameters. */
public static class TileLeaveEvent {
private final EventProto.TileLeaveEvent mProto;
diff --git a/wear/wear-tiles/src/main/java/androidx/wear/tiles/readers/RequestReaders.java b/wear/wear-tiles/src/main/java/androidx/wear/tiles/readers/RequestReaders.java
index 0584c8c..d3a50de6 100644
--- a/wear/wear-tiles/src/main/java/androidx/wear/tiles/readers/RequestReaders.java
+++ b/wear/wear-tiles/src/main/java/androidx/wear/tiles/readers/RequestReaders.java
@@ -33,7 +33,7 @@
public class RequestReaders {
private RequestReaders() {}
- /** Reader for a {@link TileRequestData} instance. */
+ /** Reader for Tile request parameters. */
public static class TileRequest {
private final RequestProto.TileRequest mProto;
private final int mTileId;
@@ -75,7 +75,7 @@
}
}
- /** Reader for a {@link ResourcesRequestData} instance. */
+ /** Reader for resource request parameters. */
public static class ResourcesRequest {
private final RequestProto.ResourcesRequest mProto;
private final int mTileId;
@@ -105,15 +105,15 @@
return mTileId;
}
- /** Get the resource version requested by this {@link ResourcesRequestData}. */
+ /** Get the requested resource version. */
@NonNull
public String getVersion() {
return mProto.getVersion();
}
/**
- * Get the resource IDs requested by this {@link ResourcesRequestData}. May be empty, in
- * which case all resources should be returned.
+ * Get the requested resource IDs. May be empty, in which case all resources should be
+ * returned.
*/
@NonNull
public List<String> getResourceIds() {
diff --git a/wear/wear-watchface-client/guava/src/androidTest/java/androidx/wear/watchface/ListenableWatchFaceControlClientTest.kt b/wear/wear-watchface-client/guava/src/androidTest/java/androidx/wear/watchface/ListenableWatchFaceControlClientTest.kt
index c3dec98..12aa604 100644
--- a/wear/wear-watchface-client/guava/src/androidTest/java/androidx/wear/watchface/ListenableWatchFaceControlClientTest.kt
+++ b/wear/wear-watchface-client/guava/src/androidTest/java/androidx/wear/watchface/ListenableWatchFaceControlClientTest.kt
@@ -20,9 +20,11 @@
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.graphics.Rect
import android.os.Handler
import android.os.IBinder
import android.os.Looper
+import android.view.Surface
import android.view.SurfaceHolder
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -34,8 +36,12 @@
import androidx.wear.watchface.samples.createExampleCanvasAnalogWatchFaceBuilder
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertNull
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
import java.util.concurrent.TimeUnit
private const val TIMEOUT_MS = 500L
@@ -44,6 +50,19 @@
@MediumTest
public class ListenableWatchFaceControlClientTest {
+ @Mock
+ private lateinit var surfaceHolder: SurfaceHolder
+ @Mock
+ private lateinit var surface: Surface
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ Mockito.`when`(surfaceHolder.surfaceFrame)
+ .thenReturn(Rect(0, 0, 400, 400))
+ Mockito.`when`(surfaceHolder.surface).thenReturn(surface)
+ }
+
@Test
public fun headlessSchemaSettingIds() {
val context = ApplicationProvider.getApplicationContext<Context>()
@@ -125,7 +144,12 @@
attachBaseContext(context)
}
}
- service.onCreateEngine()
+ service.onCreateEngine().onSurfaceChanged(
+ surfaceHolder,
+ 0,
+ surfaceHolder.surfaceFrame.width(),
+ surfaceHolder.surfaceFrame.height()
+ )
val interactiveInstance = interactiveInstanceFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
assertThat(interactiveInstance.userStyleSchema.userStyleSettings.map { it.id })
@@ -215,7 +239,12 @@
attachBaseContext(context)
}
}
- service.onCreateEngine()
+ service.onCreateEngine().onSurfaceChanged(
+ surfaceHolder,
+ 0,
+ surfaceHolder.surfaceFrame.width(),
+ surfaceHolder.surfaceFrame.height()
+ )
val interactiveInstance = interactiveInstanceFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
val headlessInstance1 = client.createHeadlessWatchFaceClient(
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 cd927f1..6548a2e 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
@@ -24,6 +24,7 @@
import android.os.Handler
import android.os.Looper
import android.service.wallpaper.WallpaperService
+import android.view.Surface
import android.view.SurfaceHolder
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -88,6 +89,8 @@
@Mock
private lateinit var surfaceHolder: SurfaceHolder
+ @Mock
+ private lateinit var surface: Surface
private lateinit var engine: WallpaperService.Engine
private val handler = Handler(Looper.getMainLooper())
private val engineLatch = CountDownLatch(1)
@@ -100,6 +103,7 @@
Mockito.`when`(surfaceHolder.surfaceFrame)
.thenReturn(Rect(0, 0, 400, 400))
+ Mockito.`when`(surfaceHolder.surface).thenReturn(surface)
}
@After
@@ -141,6 +145,12 @@
private fun createEngine() {
handler.post {
engine = wallpaperService.onCreateEngine()
+ engine.onSurfaceChanged(
+ surfaceHolder,
+ 0,
+ surfaceHolder.surfaceFrame.width(),
+ surfaceHolder.surfaceFrame.height()
+ )
engineLatch.countDown()
}
engineLatch.await(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
@@ -537,7 +547,15 @@
assertFalse(deferredExistingInstance.isCompleted)
// We don't want to leave a pending request or it'll mess up subsequent tests.
- handler.post { engine = wallpaperService.onCreateEngine() }
+ handler.post {
+ engine = wallpaperService.onCreateEngine()
+ engine.onSurfaceChanged(
+ surfaceHolder,
+ 0,
+ surfaceHolder.surfaceFrame.width(),
+ surfaceHolder.surfaceFrame.height()
+ )
+ }
runBlocking {
withTimeout(CONNECT_TIMEOUT_MILLIS) {
deferredExistingInstance.await()
diff --git a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
index 3796bda..57b4cdc 100644
--- a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
+++ b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
@@ -534,6 +534,7 @@
)
)
- assertThat(style.toString()).isEqualTo("[id1 -> 2, id2 -> 3]")
+ assertThat(style.toString()).contains("id1 -> 2")
+ assertThat(style.toString()).contains("id2 -> 3")
}
}
diff --git a/wear/wear-watchface/api/current.txt b/wear/wear-watchface/api/current.txt
index 289dc0a..193d83b 100644
--- a/wear/wear-watchface/api/current.txt
+++ b/wear/wear-watchface/api/current.txt
@@ -79,12 +79,12 @@
public final class ComplicationOutlineRenderer {
ctor public ComplicationOutlineRenderer();
- method public static void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
+ method public static void drawComplicationOutline(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, @ColorInt int color);
+ method public void drawComplicationOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
}
public final class ComplicationsManager {
diff --git a/wear/wear-watchface/api/public_plus_experimental_current.txt b/wear/wear-watchface/api/public_plus_experimental_current.txt
index 289dc0a..193d83b 100644
--- a/wear/wear-watchface/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface/api/public_plus_experimental_current.txt
@@ -79,12 +79,12 @@
public final class ComplicationOutlineRenderer {
ctor public ComplicationOutlineRenderer();
- method public static void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
+ method public static void drawComplicationOutline(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, @ColorInt int color);
+ method public void drawComplicationOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
}
public final class ComplicationsManager {
diff --git a/wear/wear-watchface/api/restricted_current.txt b/wear/wear-watchface/api/restricted_current.txt
index eb61cbf..2584372 100644
--- a/wear/wear-watchface/api/restricted_current.txt
+++ b/wear/wear-watchface/api/restricted_current.txt
@@ -79,12 +79,12 @@
public final class ComplicationOutlineRenderer {
ctor public ComplicationOutlineRenderer();
- method public static void drawComplicationSelectOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
+ method public static void drawComplicationOutline(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, @ColorInt int color);
+ method public void drawComplicationOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, @ColorInt int color);
}
public final class ComplicationsManager {
diff --git a/wear/wear-watchface/samples/minimal/build.gradle b/wear/wear-watchface/samples/minimal/build.gradle
new file mode 100644
index 0000000..c1464dd
--- /dev/null
+++ b/wear/wear-watchface/samples/minimal/build.gradle
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryType
+import androidx.build.LibraryVersions
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.application")
+}
+
+dependencies {
+ api(project(":wear:wear-watchface"))
+ api(project(":wear:wear-watchface-guava"))
+ implementation(GUAVA_ANDROID)
+}
+
+androidx {
+ name = "AndroidX Wear Watchface Minimal Sample"
+ type = LibraryType.SAMPLES
+ mavenGroup = LibraryGroups.WEAR
+ mavenVersion = LibraryVersions.WEAR_WATCHFACE
+ inceptionYear = "2021"
+ description = "Contains the sample code for the Androidx Wear Watchface library"
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 25
+ }
+ compileOptions {
+ sourceCompatibility 1.8
+ targetCompatibility 1.8
+ }
+}
diff --git a/wear/wear-watchface/samples/minimal/src/main/AndroidManifest.xml b/wear/wear-watchface/samples/minimal/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..4c45d9c
--- /dev/null
+++ b/wear/wear-watchface/samples/minimal/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.wear.watchface.samples.minimal">
+
+ <uses-feature android:name="android.hardware.type.watch" />
+
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:theme="@android:style/Theme.DeviceDefault"
+ android:fullBackupContent="false">
+
+ <service
+ android:name=".WatchFaceService"
+ android:directBootAware="true"
+ android:exported="true"
+ android:label="@string/app_name"
+ android:permission="android.permission.BIND_WALLPAPER">
+
+ <intent-filter>
+ <action android:name="android.service.wallpaper.WallpaperService" />
+ <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
+ </intent-filter>
+
+ <meta-data
+ android:name="com.google.android.wearable.watchface.preview"
+ android:resource="@drawable/preview" />
+
+ <meta-data
+ android:name="android.service.wallpaper"
+ android:resource="@xml/watch_face" />
+
+ </service>
+
+ </application>
+
+</manifest>
diff --git a/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceRenderer.java b/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceRenderer.java
new file mode 100644
index 0000000..5271dda
--- /dev/null
+++ b/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceRenderer.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.wear.watchface.samples.minimal;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Rect;
+import android.icu.util.Calendar;
+import android.view.SurfaceHolder;
+
+import androidx.wear.watchface.CanvasType;
+import androidx.wear.watchface.Renderer;
+import androidx.wear.watchface.WatchState;
+import androidx.wear.watchface.style.UserStyleRepository;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Minimal rendered for the watch face, using canvas to render hours, minutes, and a blinking
+ * separator.
+ */
+public class WatchFaceRenderer extends Renderer.CanvasRenderer {
+
+ private static final long UPDATE_DELAY_MILLIS = TimeUnit.SECONDS.toMillis(1);
+ private static final char[] DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
+
+ private final Paint mPaint;
+ private final char[] mTimeText = new char[5];
+
+ public WatchFaceRenderer(
+ @NotNull SurfaceHolder surfaceHolder,
+ @NotNull UserStyleRepository userStyleRepository,
+ @NotNull WatchState watchState) {
+ super(surfaceHolder, userStyleRepository, watchState, CanvasType.HARDWARE,
+ UPDATE_DELAY_MILLIS);
+ mPaint = new Paint();
+ mPaint.setTextAlign(Align.CENTER);
+ mPaint.setTextSize(64f);
+ }
+
+ @Override
+ public void render(@NotNull Canvas canvas, @NotNull Rect rect, @NotNull Calendar calendar) {
+ mPaint.setColor(Color.BLACK);
+ canvas.drawRect(rect, mPaint);
+ mPaint.setColor(Color.WHITE);
+ int hour = calendar.get(Calendar.HOUR_OF_DAY);
+ int minute = calendar.get(Calendar.MINUTE);
+ int second = calendar.get(Calendar.SECOND);
+ mTimeText[0] = DIGITS[hour / 10];
+ mTimeText[1] = DIGITS[hour % 10];
+ mTimeText[2] = second % 2 == 0 ? ':' : ' ';
+ mTimeText[3] = DIGITS[minute / 10];
+ mTimeText[4] = DIGITS[minute % 10];
+ canvas.drawText(mTimeText, 0, 5, rect.centerX(), rect.centerY(), mPaint);
+ }
+}
diff --git a/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceService.java b/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceService.java
new file mode 100644
index 0000000..5ceede1
--- /dev/null
+++ b/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceService.java
@@ -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.wear.watchface.samples.minimal;
+
+import android.view.SurfaceHolder;
+
+import androidx.wear.watchface.Complication;
+import androidx.wear.watchface.ComplicationsManager;
+import androidx.wear.watchface.ListenableWatchFaceService;
+import androidx.wear.watchface.Renderer;
+import androidx.wear.watchface.WatchFace;
+import androidx.wear.watchface.WatchFaceType;
+import androidx.wear.watchface.WatchState;
+import androidx.wear.watchface.style.UserStyleRepository;
+import androidx.wear.watchface.style.UserStyleSchema;
+import androidx.wear.watchface.style.UserStyleSetting;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collections;
+
+/** The service that defines the watch face. */
+public class WatchFaceService extends ListenableWatchFaceService {
+
+ @NotNull
+ @Override
+ protected ListenableFuture<WatchFace> createWatchFaceFuture(
+ @NotNull SurfaceHolder surfaceHolder, @NotNull WatchState watchState) {
+ UserStyleRepository userStyleRepository =
+ new UserStyleRepository(
+ new UserStyleSchema(Collections.<UserStyleSetting>emptyList()));
+ ComplicationsManager complicationManager =
+ new ComplicationsManager(Collections.<Complication>emptyList(),
+ userStyleRepository);
+ Renderer renderer = new WatchFaceRenderer(surfaceHolder, userStyleRepository, watchState);
+ return Futures.immediateFuture(
+ new WatchFace(WatchFaceType.DIGITAL, userStyleRepository, renderer,
+ complicationManager));
+ }
+}
diff --git a/wear/wear-watchface/samples/minimal/src/main/res/drawable/ic_launcher_background.xml b/wear/wear-watchface/samples/minimal/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..69d8a24
--- /dev/null
+++ b/wear/wear-watchface/samples/minimal/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <group android:scaleX="0.7"
+ android:scaleY="0.7"
+ android:translateX="16.2"
+ android:translateY="16.2">
+ <path android:fillColor="#3DDC84"
+ android:pathData="M0,0h108v108h-108z"/>
+ <path android:fillColor="#00000000" android:pathData="M9,0L9,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,0L19,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M29,0L29,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M39,0L39,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M49,0L49,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M59,0L59,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M69,0L69,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M79,0L79,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M89,0L89,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M99,0L99,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,9L108,9"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,19L108,19"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,29L108,29"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,39L108,39"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,49L108,49"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,59L108,59"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,69L108,69"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,79L108,79"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,89L108,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,99L108,99"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,29L89,29"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,39L89,39"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,49L89,49"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,59L89,59"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,69L89,69"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,79L89,79"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M29,19L29,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M39,19L39,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M49,19L49,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M59,19L59,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M69,19L69,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M79,19L79,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ </group>
+</vector>
diff --git a/wear/wear-watchface/samples/minimal/src/main/res/drawable/ic_launcher_foreground.xml b/wear/wear-watchface/samples/minimal/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..74f38c8
--- /dev/null
+++ b/wear/wear-watchface/samples/minimal/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,15 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108"
+ android:tint="#000000">
+ <group android:scaleX="2.61"
+ android:scaleY="2.61"
+ android:translateX="22.68"
+ android:translateY="22.68">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10s10,-4.5 10,-10S17.5,2 12,2zM16.2,16.2L11,13V7h1.5v5.2l4.5,2.7L16.2,16.2z"/>
+ </group>
+</vector>
diff --git a/wear/wear-watchface/samples/minimal/src/main/res/drawable/preview.png b/wear/wear-watchface/samples/minimal/src/main/res/drawable/preview.png
new file mode 100644
index 0000000..24eadcb
--- /dev/null
+++ b/wear/wear-watchface/samples/minimal/src/main/res/drawable/preview.png
Binary files differ
diff --git a/wear/wear-watchface/samples/minimal/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/wear/wear-watchface/samples/minimal/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..bbd3e02
--- /dev/null
+++ b/wear/wear-watchface/samples/minimal/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background"/>
+ <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon>
\ No newline at end of file
diff --git a/wear/wear-watchface/samples/minimal/src/main/res/mipmap-hdpi/ic_launcher.png b/wear/wear-watchface/samples/minimal/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..837401a
--- /dev/null
+++ b/wear/wear-watchface/samples/minimal/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/wear/wear-watchface/samples/minimal/src/main/res/mipmap-mdpi/ic_launcher.png b/wear/wear-watchface/samples/minimal/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..f208553
--- /dev/null
+++ b/wear/wear-watchface/samples/minimal/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/wear/wear-watchface/samples/minimal/src/main/res/mipmap-xhdpi/ic_launcher.png b/wear/wear-watchface/samples/minimal/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..c9f75b4
--- /dev/null
+++ b/wear/wear-watchface/samples/minimal/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/wear/wear-watchface/samples/minimal/src/main/res/mipmap-xxhdpi/ic_launcher.png b/wear/wear-watchface/samples/minimal/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..5c6c342
--- /dev/null
+++ b/wear/wear-watchface/samples/minimal/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/wear/wear-watchface/samples/minimal/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/wear/wear-watchface/samples/minimal/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4b7cddf
--- /dev/null
+++ b/wear/wear-watchface/samples/minimal/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/wear/wear-watchface/samples/minimal/src/main/res/values/strings.xml b/wear/wear-watchface/samples/minimal/src/main/res/values/strings.xml
new file mode 100644
index 0000000..1c8f26a
--- /dev/null
+++ b/wear/wear-watchface/samples/minimal/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+<resources>
+ <string name="app_name">Minimal Digital</string>
+</resources>
diff --git a/wear/wear-watchface/samples/minimal/src/main/res/xml/watch_face.xml b/wear/wear-watchface/samples/minimal/src/main/res/xml/watch_face.xml
new file mode 100644
index 0000000..7e7098f
--- /dev/null
+++ b/wear/wear-watchface/samples/minimal/src/main/res/xml/watch_face.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<wallpaper/>
\ No newline at end of file
diff --git a/wear/wear-watchface/src/androidTest/AndroidManifest.xml b/wear/wear-watchface/src/androidTest/AndroidManifest.xml
index 69502ff..b18d5a3c 100644
--- a/wear/wear-watchface/src/androidTest/AndroidManifest.xml
+++ b/wear/wear-watchface/src/androidTest/AndroidManifest.xml
@@ -17,6 +17,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="androidx.wear.watchface.test">
<application android:requestLegacyExternalStorage="true">
+ <activity android:name=".ComplicationTapActivity"/>
</application>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
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 55b5c24..b82b434 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
@@ -40,7 +40,9 @@
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
+import androidx.wear.watchface.samples.EXAMPLE_OPENGL_COMPLICATION_ID
import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService
+import androidx.wear.watchface.samples.ExampleOpenGLWatchFaceService
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -80,6 +82,32 @@
)
}
+ private fun createOpenGlInstance(width: Int, height: Int): IHeadlessWatchFace {
+ val instanceService = IWatchFaceControlService.Stub.asInterface(
+ WatchFaceControlService().apply {
+ setContext(ApplicationProvider.getApplicationContext<Context>())
+ }.onBind(
+ Intent(WatchFaceControlService.ACTION_WATCHFACE_CONTROL_SERVICE)
+ )
+ )
+ return instanceService.createHeadlessWatchFaceInstance(
+ HeadlessWatchFaceInstanceParams(
+ ComponentName(
+ ApplicationProvider.getApplicationContext<Context>(),
+ ExampleOpenGLWatchFaceService::class.java
+ ),
+ DeviceConfig(
+ false,
+ false,
+ 0,
+ 0
+ ),
+ width,
+ height
+ )
+ )
+ }
+
@Test
fun createHeadlessWatchFaceInstance() {
val instance = createInstance(100, 100)
@@ -124,6 +152,40 @@
}
@Test
+ fun createHeadlessOpenglWatchFaceInstance() {
+ val instance = createOpenGlInstance(400, 400)
+ val bitmap = SharedMemoryImage.ashmemReadImageBundle(
+ instance.takeWatchFaceScreenshot(
+ WatchfaceScreenshotParams(
+ RenderParameters(
+ DrawMode.INTERACTIVE,
+ RenderParameters.DRAW_ALL_LAYERS,
+ null,
+ Color.RED
+ ).toWireFormat(),
+ 1234567890,
+ null,
+ listOf(
+ IdAndComplicationDataWireFormat(
+ EXAMPLE_OPENGL_COMPLICATION_ID,
+ ShortTextComplicationData.Builder(
+ PlainComplicationText.Builder("Mon").build()
+ )
+ .setTitle(PlainComplicationText.Builder("23rd").build())
+ .build()
+ .asWireComplicationData()
+ )
+ )
+ )
+ )
+ )
+
+ bitmap.assertAgainstGolden(screenshotRule, "opengl_headless")
+
+ instance.release()
+ }
+
+ @Test
fun testCommandTakeComplicationScreenShot() {
val instance = createInstance(400, 400)
val bitmap = SharedMemoryImage.ashmemReadImageBundle(
@@ -153,4 +215,4 @@
instance.release()
}
-}
+}
\ No newline at end of file
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 caeba39..ab591af 100644
--- a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
+++ b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
@@ -16,13 +16,17 @@
package androidx.wear.watchface.test
+import android.app.Activity
+import android.app.PendingIntent
import android.content.Context
+import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Rect
import android.graphics.SurfaceTexture
import android.icu.util.TimeZone
+import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.support.wearable.watchface.SharedMemoryImage
@@ -39,6 +43,7 @@
import androidx.wear.watchface.DrawMode
import androidx.wear.watchface.LayerMode
import androidx.wear.watchface.RenderParameters
+import androidx.wear.watchface.TapType
import androidx.wear.watchface.WatchFaceService
import androidx.wear.watchface.control.IInteractiveWatchFaceWCS
import androidx.wear.watchface.control.IPendingInteractiveWatchFaceWCS
@@ -54,6 +59,7 @@
import androidx.wear.watchface.samples.GREEN_STYLE
import androidx.wear.watchface.style.Layer
import androidx.wear.watchface.style.data.UserStyleWireFormat
+import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -71,6 +77,36 @@
private const val INTERACTIVE_INSTANCE_ID = "InteractiveTestInstance"
+// Activity for testing complication taps.
+public class ComplicationTapActivity : Activity() {
+ internal companion object {
+ private val lock = Any()
+ private lateinit var theIntent: Intent
+ private var countDown: CountDownLatch? = null
+
+ fun newCountDown() {
+ countDown = CountDownLatch(1)
+ }
+
+ fun awaitIntent(): Intent? {
+ if (countDown!!.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+ return theIntent
+ } else {
+ return null
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ synchronized(lock) {
+ theIntent = intent
+ }
+ countDown!!.countDown()
+ finish()
+ }
+}
+
@RunWith(AndroidJUnit4::class)
@MediumTest
class WatchFaceServiceImageTest {
@@ -87,6 +123,18 @@
SystemProviders.DAY_OF_WEEK to
ShortTextComplicationData.Builder(PlainComplicationText.Builder("Mon").build())
.setTitle(PlainComplicationText.Builder("23rd").build())
+ .setTapAction(
+ PendingIntent.getActivity(
+ ApplicationProvider.getApplicationContext<Context>(),
+ 123,
+ Intent(
+ ApplicationProvider.getApplicationContext<Context>(),
+ ComplicationTapActivity::class.java
+ ).apply {
+ },
+ PendingIntent.FLAG_ONE_SHOT
+ )
+ )
.build()
.asWireComplicationData(),
SystemProviders.STEP_COUNT to
@@ -146,6 +194,12 @@
engineWrapper =
canvasAnalogWatchFaceService.onCreateEngine() as WatchFaceService.EngineWrapper
+ engineWrapper.onSurfaceChanged(
+ surfaceHolder,
+ 0,
+ surfaceHolder.surfaceFrame.width(),
+ surfaceHolder.surfaceFrame.height()
+ )
}
private fun initGles2WatchFace() {
@@ -166,6 +220,12 @@
setPendingWallpaperInteractiveWatchFaceInstance()
engineWrapper = glesWatchFaceService.onCreateEngine() as WatchFaceService.EngineWrapper
+ engineWrapper.onSurfaceChanged(
+ surfaceHolder,
+ 0,
+ surfaceHolder.surfaceFrame.width(),
+ surfaceHolder.surfaceFrame.height()
+ )
}
private fun setPendingWallpaperInteractiveWatchFaceInstance() {
@@ -508,6 +568,12 @@
)
engineWrapper = service.onCreateEngine() as WatchFaceService.EngineWrapper
+ engineWrapper.onSurfaceChanged(
+ surfaceHolder,
+ 0,
+ surfaceHolder.surfaceFrame.width(),
+ surfaceHolder.surfaceFrame.height()
+ )
handler.post { engineWrapper.draw() }
}
@@ -518,4 +584,25 @@
engineWrapper.onDestroy()
}
}
+
+ @Test
+ fun complicationTapLaunchesActivity() {
+ handler.post(this::initCanvasWatchFace)
+
+ ComplicationTapActivity.newCountDown()
+ handler.post {
+ val interactiveWatchFaceInstanceSysUi =
+ InteractiveInstanceManager.getAndRetainInstance(
+ interactiveWatchFaceInstanceWCS.instanceId
+ )!!.createSysUiApi()
+ interactiveWatchFaceInstanceSysUi.sendTouchEvent(
+ 85,
+ 165,
+ TapType.TAP
+ )
+ interactiveWatchFaceInstanceSysUi.release()
+ }
+
+ assertThat(ComplicationTapActivity.awaitIntent()).isNotNull()
+ }
}
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 acfc804..4c55fd3 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
@@ -200,7 +200,7 @@
@ColorInt color: Int
) {
if (!attachedComplication!!.fixedComplicationProvider) {
- ComplicationOutlineRenderer.drawComplicationSelectOutline(
+ ComplicationOutlineRenderer.drawComplicationOutline(
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 befcb84..2467a50 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
@@ -44,7 +44,7 @@
/** Draws a thick line around the complication with the given bounds. */
@JvmStatic
- public fun drawComplicationSelectOutline(
+ public fun drawComplicationOutline(
canvas: Canvas,
bounds: Rect,
@ColorInt color: Int
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 5417b77..a1df4f19 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
@@ -476,13 +476,23 @@
Log.w(TAG, "eglDestroySurface failed")
}
}
- eglSurface = EGL14.eglCreateWindowSurface(
- eglDisplay,
- eglConfig,
- surfaceHolder.surface,
- eglSurfaceAttribList,
- 0
- )
+ eglSurface = if (watchState.isHeadless) {
+ // Headless instances have a fake surfaceHolder so fall back to a Pbuffer.
+ EGL14.eglCreatePbufferSurface(
+ eglDisplay,
+ eglConfig,
+ intArrayOf(EGL14.EGL_WIDTH, width, EGL14.EGL_HEIGHT, height, EGL14.EGL_NONE),
+ 0
+ )
+ } else {
+ EGL14.eglCreateWindowSurface(
+ eglDisplay,
+ eglConfig,
+ surfaceHolder.surface,
+ eglSurfaceAttribList,
+ 0
+ )
+ }
if (eglSurface == EGL14.EGL_NO_SURFACE) {
throw RuntimeException("eglCreateWindowSurface failed")
}
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 ee75eb8..2c5d45d 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
@@ -352,17 +352,19 @@
private var pendingVisibilityChanged: Boolean? = null
private var pendingComplicationDataUpdates = ArrayList<PendingComplicationData>()
private var complicationsActivated = false
+ private var watchFaceInitStarted = false
// Only valid after onSetBinder has been called.
private var systemApiVersion = -1
internal var firstSetSystemState = true
internal var immutableSystemStateDone = false
+ private var ignoreNextOnVisibilityChanged = false
internal var lastActiveComplications: IntArray? = null
internal var lastA11yLabels: Array<ContentDescriptionLabel>? = null
- private var watchFaceInitStarted = false
+ private var firstOnSurfaceChangedReceived = false
private var asyncWatchFaceConstructionPending = false
private var initialUserStyle: UserStyleWireFormat? = null
@@ -370,17 +372,14 @@
private var createdBy = "?"
- init {
- TraceEvent("EngineWrapper.init").use {
- // If this is a headless instance then we don't want to create a WCS instance.
- if (!mutableWatchState.isHeadless) {
- maybeCreateWCSApi()
- }
- }
- }
-
+ /** Note this function should only be called once. */
@SuppressWarnings("NewApi")
- private fun maybeCreateWCSApi() {
+ private fun maybeCreateWCSApi(): Unit = TraceEvent("EngineWrapper.maybeCreateWCSApi").use {
+ // If this is a headless instance then we don't want to create a WCS instance.
+ if (mutableWatchState.isHeadless) {
+ return
+ }
+
val pendingWallpaperInstance =
InteractiveInstanceManager.takePendingWallpaperInteractiveWatchFaceInstance()
@@ -406,6 +405,12 @@
if (pendingWallpaperInstance != null) {
val asyncTraceEvent =
AsyncTraceEvent("Create PendingWallpaperInteractiveWatchFaceInstance")
+ // The WallpaperService works around bugs in wallpapers (see b/5233826 and
+ // b/5209847) by sending onVisibilityChanged(true), onVisibilityChanged(false)
+ // after onSurfaceChanged during creation. This is unfortunate for us since we
+ // perform work in response (see WatchFace.visibilityObserver). So here we
+ // workaround the workaround...
+ ignoreNextOnVisibilityChanged = true
coroutineScope.launch {
pendingWallpaperInstance.callback.onInteractiveWatchFaceWcsCreated(
createInteractiveInstance(
@@ -747,6 +752,23 @@
)
}
+ override fun onSurfaceChanged(
+ holder: SurfaceHolder?,
+ format: Int,
+ width: Int,
+ height: Int
+ ): Unit = TraceEvent("EngineWrapper.onSurfaceChanged").use {
+ super.onSurfaceChanged(holder, format, width, height)
+
+ // We can only call maybeCreateWCSApi once. For OpenGL watch faces we need to wait for
+ // onSurfaceChanged before bootstrapping because the surface isn't valid for creating
+ // an EGL context until then.
+ if (!firstOnSurfaceChangedReceived) {
+ maybeCreateWCSApi()
+ firstOnSurfaceChangedReceived = true
+ }
+ }
+
override fun onDestroy(): Unit = TraceEvent("EngineWrapper.onDestroy").use {
destroyed = true
uiThreadHandler.removeCallbacks(invalidateRunnable)
@@ -1086,22 +1108,34 @@
).use {
super.onVisibilityChanged(visible)
- // We are requesting state every time the watch face changes its visibility because
- // wallpaper commands have a tendency to be dropped. By requesting it on every
- // visibility change, we ensure that we don't become a victim of some race condition.
- sendBroadcast(
- Intent(Constants.ACTION_REQUEST_STATE).apply {
- putExtra(Constants.EXTRA_WATCH_FACE_VISIBLE, visible)
- }
- )
-
- // We can't guarantee the binder has been set and onSurfaceChanged called before this
- // command.
- if (!watchFaceCreated()) {
- pendingVisibilityChanged = visible
+ if (ignoreNextOnVisibilityChanged) {
+ ignoreNextOnVisibilityChanged = false
return
}
+ // In the WSL flow Home doesn't know when WallpaperService has actually launched a
+ // watchface after requesting a change. It used [Constants.ACTION_REQUEST_STATE] as a
+ // signal to trigger the old boot flow (sending the binder etc). This is no longer
+ // required from android R onwards. See (b/181965946).
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ // We are requesting state every time the watch face changes its visibility because
+ // wallpaper commands have a tendency to be dropped. By requesting it on every
+ // visibility change, we ensure that we don't become a victim of some race
+ // condition.
+ sendBroadcast(
+ Intent(Constants.ACTION_REQUEST_STATE).apply {
+ putExtra(Constants.EXTRA_WATCH_FACE_VISIBLE, visible)
+ }
+ )
+
+ // We can't guarantee the binder has been set and onSurfaceChanged called before
+ // this command.
+ if (!watchFaceCreated()) {
+ pendingVisibilityChanged = visible
+ return
+ }
+ }
+
mutableWatchState.isVisible.value = visible
pendingVisibilityChanged = null
}
@@ -1273,8 +1307,10 @@
}
}
writer.println("createdBy=$createdBy")
+ writer.println("firstOnSurfaceChanged=$firstOnSurfaceChangedReceived")
writer.println("watchFaceInitStarted=$watchFaceInitStarted")
writer.println("asyncWatchFaceConstructionPending=$asyncWatchFaceConstructionPending")
+ writer.println("ignoreNextOnVisibilityChanged=$ignoreNextOnVisibilityChanged")
if (this::interactiveInstanceId.isInitialized) {
writer.println("interactiveInstanceId=$interactiveInstanceId")
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
index 718dd4c..719ede7 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
@@ -91,6 +91,7 @@
@RunWith(WatchFaceTestRunner::class)
public class AsyncWatchFaceInitTest {
private val handler = mock<Handler>()
+ private val surfaceHolder = mock<SurfaceHolder>()
private var looperTimeMillis = 0L
private val pendingTasks = PriorityQueue<Task>()
private val userStyleRepository = UserStyleRepository(UserStyleSchema(emptyList()))
@@ -176,6 +177,7 @@
)
val engineWrapper = service.onCreateEngine() as WatchFaceService.EngineWrapper
+ engineWrapper.onSurfaceChanged(surfaceHolder, 0, 100, 100)
runPostedTasksFor(0)
@@ -215,7 +217,8 @@
initParams
)
- service.onCreateEngine() as WatchFaceService.EngineWrapper
+ val engineWrapper = service.onCreateEngine() as WatchFaceService.EngineWrapper
+ engineWrapper.onSurfaceChanged(surfaceHolder, 0, 100, 100)
runPostedTasksFor(0)
var pendingInteractiveWatchFaceWcs: IInteractiveWatchFaceWCS? = null
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 13f1879..5e7c56a 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
@@ -2279,7 +2279,7 @@
)
)
- service.onCreateEngine()
+ service.onCreateEngine().onSurfaceChanged(surfaceHolder, 0, 100, 100)
runPendingPostedDispatchedContinuationTasks()
@@ -2337,4 +2337,43 @@
val instance = InteractiveInstanceManager.getAndRetainInstance(instanceId)
assertThat(instance).isNull()
}
+
+ public fun firstOnVisibilityChangedIgnoredPostRFlow() {
+ val instanceId = "interactiveInstanceId"
+ initWallpaperInteractiveWatchFaceInstance(
+ WatchFaceType.ANALOG,
+ listOf(leftComplication, rightComplication),
+ UserStyleSchema(emptyList()),
+ WallpaperInteractiveWatchFaceInstanceParams(
+ instanceId,
+ DeviceConfig(
+ false,
+ false,
+ 0,
+ 0
+ ),
+ SystemState(false, 0),
+ UserStyle(emptyMap()).toWireFormat(),
+ listOf(
+ IdAndComplicationDataWireFormat(
+ LEFT_COMPLICATION_ID,
+ ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
+ .setShortText(ComplicationText.plainText("INITIAL_VALUE"))
+ .build()
+ )
+ )
+ )
+ )
+
+ val observer = mock<Observer<Boolean>>()
+
+ // This should be ignored.
+ engineWrapper.onVisibilityChanged(true)
+ watchState.isVisible.addObserver(observer)
+ verify(observer, times(0)).onChanged(false)
+
+ // This should trigger the observer.
+ engineWrapper.onVisibilityChanged(false)
+ verify(observer).onChanged(true)
+ }
}
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/WearArcLayoutTest.kt b/wear/wear/src/androidTest/java/androidx/wear/widget/WearArcLayoutTest.kt
index 54988dd..47b72bb 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/WearArcLayoutTest.kt
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/WearArcLayoutTest.kt
@@ -46,6 +46,7 @@
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.screenshot.AndroidXScreenshotTestRule
@@ -633,6 +634,7 @@
testEventsFast("touch_fast_screenshot", views)
}
+ @FlakyTest // b/182268136
@Test(timeout = 5000)
fun testMarginTouch() {
val views = createTwoArcsWithMargin()
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
index e39e5ca..bc7e5b5 100644
--- a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionFoldingFeature.java
+++ b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionFoldingFeature.java
@@ -101,7 +101,7 @@
private final int mState;
public ExtensionFoldingFeature(@NonNull Rect bounds, @Type int type, @State int state) {
- validateFeatureBounds(bounds, type);
+ validateFeatureBounds(bounds);
mBounds = new Rect(bounds);
mType = type;
mState = state;
@@ -129,26 +129,13 @@
/**
* Verifies the bounds of the folding feature.
*/
- private static void validateFeatureBounds(@NonNull Rect bounds, int type) {
+ private static void validateFeatureBounds(@NonNull Rect bounds) {
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));
- }
+ if (bounds.left != 0 && bounds.top != 0) {
+ throw new IllegalArgumentException("Bounding rectangle must start at the top or "
+ + "left window edge for folding features");
}
}
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 264cd13..7dee369 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,7 +22,6 @@
import android.widget.FrameLayout
import android.widget.TextView
import androidx.core.util.Consumer
-import androidx.window.FoldingFeature
import androidx.window.WindowLayoutInfo
import androidx.window.WindowManager
import java.text.SimpleDateFormat
@@ -94,12 +93,7 @@
}
val featureView = View(this)
- 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)
- }
+ val color = getColor(R.color.colorFeatureFold)
featureView.foreground = ColorDrawable(color)
rootLayout.addView(featureView, lp)
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 cc0b341..583b46e 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
@@ -25,8 +25,6 @@
import android.widget.FrameLayout
import androidx.window.DisplayFeature
import androidx.window.FoldingFeature
-import androidx.window.FoldingFeature.TYPE_FOLD
-import androidx.window.FoldingFeature.TYPE_HINGE
import androidx.window.WindowLayoutInfo
/**
@@ -195,7 +193,6 @@
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
+ return 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 3ea6946..af7dbca 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
@@ -57,22 +57,11 @@
}
/**
- * @return {@link DeviceState} with an unknown posture.
- * @deprecated Will be removed when method is removed from {@link WindowBackend}
- */
- @Deprecated("Added for compatibility with WindowBackend in sample")
- override fun getDeviceState(): DeviceState {
- return DeviceState.Builder().setPosture(DeviceState.POSTURE_OPENED).build()
- }
-
- /**
* @param activity Currently running {@link Activity}.
* @return A fake {@link WindowLayoutInfo} with a fold in the middle matching the {@link
* FoldAxis}.
- * @deprecated Visibility will be reduced when method is removed from {@link WindowBackend}.
*/
- @Deprecated("Exposed for compatibility with WindowBackend in sample")
- override fun getWindowLayoutInfo(activity: Activity): WindowLayoutInfo {
+ private fun getWindowLayoutInfo(activity: Activity): WindowLayoutInfo {
val windowSize = activity.calculateWindowSizeExt()
val featureRect = foldRect(windowSize)
@@ -88,11 +77,6 @@
}
@Deprecated("Added for compatibility with WindowBackend in sample")
- override fun getWindowLayoutInfo(context: Context): WindowLayoutInfo {
- return WindowLayoutInfo.Builder().setDisplayFeatures(emptyList()).build()
- }
-
- @Deprecated("Added for compatibility with WindowBackend in sample")
override fun registerLayoutChangeCallback(
context: Context,
executor: Executor,
diff --git a/window/window-samples/src/main/res/layout/activity_display_features.xml b/window/window-samples/src/main/res/layout/activity_display_features.xml
index cb3c351..cabe1f4 100644
--- a/window/window-samples/src/main/res/layout/activity_display_features.xml
+++ b/window/window-samples/src/main/res/layout/activity_display_features.xml
@@ -63,44 +63,6 @@
android:text="@string/fold" />
</LinearLayout>
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="horizontal">
-
- <ImageView
- android:id="@+id/hingeColorImageView"
- android:layout_width="20dp"
- android:layout_height="20dp"
- android:foreground="@color/colorFeatureHinge" />
-
- <TextView
- android:id="@+id/hingeColorTextView"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:text="@string/hinge" />
- </LinearLayout>
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="horizontal">
-
- <ImageView
- android:id="@+id/unknownColorImageView"
- android:layout_width="20dp"
- android:layout_height="20dp"
- android:foreground="@color/colorFeatureUnknown" />
-
- <TextView
- android:id="@+id/unknownColorTextView"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:text="@string/unknown" />
- </LinearLayout>
-
</LinearLayout>
<TextView
diff --git a/window/window-samples/src/main/res/values/colors.xml b/window/window-samples/src/main/res/values/colors.xml
index 483f736..41a72b2 100644
--- a/window/window-samples/src/main/res/values/colors.xml
+++ b/window/window-samples/src/main/res/values/colors.xml
@@ -21,8 +21,6 @@
<color name="colorAccent">#03DAC5</color>
<color name="colorFeatureFold">#7700FF00</color>
- <color name="colorFeatureHinge">#77FF0000</color>
- <color name="colorFeatureUnknown">#77000000</color>
<color name="colorSplitContentBackground">#3B6BDB4C</color>
<color name="colorSplitControlsBackground">#475ABFF3</color>
diff --git a/window/window-samples/src/main/res/values/strings.xml b/window/window-samples/src/main/res/values/strings.xml
index e5ad108..9dd6a26 100644
--- a/window/window-samples/src/main/res/values/strings.xml
+++ b/window/window-samples/src/main/res/values/strings.xml
@@ -20,8 +20,6 @@
<string name="stateUpdateLog">State update log</string>
<string name="deviceState">Device state</string>
<string name="windowLayout">Window layout</string>
- <string name="hinge">Hinge</string>
- <string name="unknown">Unknown</string>
<string name="fold">Fold</string>
<string name="legend">Legend:</string>
<string name="content_title">Content title</string>
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index 52c9682..e8f74ed 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -33,8 +33,15 @@
public class FoldingFeature implements androidx.window.DisplayFeature {
ctor public FoldingFeature(android.graphics.Rect, int, int);
method public android.graphics.Rect getBounds();
+ method public int getOcclusionMode();
+ method public int getOrientation();
method public int getState();
- method public int getType();
+ method @Deprecated public int getType();
+ method public boolean isSeparating();
+ field public static final int OCCLUSION_FULL = 1; // 0x1
+ field public static final int OCCLUSION_NONE = 0; // 0x0
+ field public static final int ORIENTATION_HORIZONTAL = 1; // 0x1
+ field public static final int ORIENTATION_VERTICAL = 0; // 0x0
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
@@ -43,9 +50,6 @@
}
public interface WindowBackend {
- method @Deprecated public androidx.window.DeviceState getDeviceState();
- method @Deprecated public androidx.window.WindowLayoutInfo getWindowLayoutInfo(android.app.Activity);
- method @Deprecated public androidx.window.WindowLayoutInfo getWindowLayoutInfo(android.content.Context);
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!>);
method @Deprecated public void registerLayoutChangeCallback(android.content.Context, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
@@ -65,14 +69,10 @@
public final class WindowManager {
ctor public WindowManager(android.content.Context);
- ctor @Deprecated public WindowManager(android.content.Context, androidx.window.WindowBackend?);
+ ctor public WindowManager(android.content.Context, androidx.window.WindowBackend);
method public androidx.window.WindowMetrics getCurrentWindowMetrics();
- method @Deprecated public androidx.window.DeviceState getDeviceState();
method public androidx.window.WindowMetrics getMaximumWindowMetrics();
- method @Deprecated public androidx.window.WindowLayoutInfo getWindowLayoutInfo();
- 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 @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 52c9682..e8f74ed 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -33,8 +33,15 @@
public class FoldingFeature implements androidx.window.DisplayFeature {
ctor public FoldingFeature(android.graphics.Rect, int, int);
method public android.graphics.Rect getBounds();
+ method public int getOcclusionMode();
+ method public int getOrientation();
method public int getState();
- method public int getType();
+ method @Deprecated public int getType();
+ method public boolean isSeparating();
+ field public static final int OCCLUSION_FULL = 1; // 0x1
+ field public static final int OCCLUSION_NONE = 0; // 0x0
+ field public static final int ORIENTATION_HORIZONTAL = 1; // 0x1
+ field public static final int ORIENTATION_VERTICAL = 0; // 0x0
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
@@ -43,9 +50,6 @@
}
public interface WindowBackend {
- method @Deprecated public androidx.window.DeviceState getDeviceState();
- method @Deprecated public androidx.window.WindowLayoutInfo getWindowLayoutInfo(android.app.Activity);
- method @Deprecated public androidx.window.WindowLayoutInfo getWindowLayoutInfo(android.content.Context);
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!>);
method @Deprecated public void registerLayoutChangeCallback(android.content.Context, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
@@ -65,14 +69,10 @@
public final class WindowManager {
ctor public WindowManager(android.content.Context);
- ctor @Deprecated public WindowManager(android.content.Context, androidx.window.WindowBackend?);
+ ctor public WindowManager(android.content.Context, androidx.window.WindowBackend);
method public androidx.window.WindowMetrics getCurrentWindowMetrics();
- method @Deprecated public androidx.window.DeviceState getDeviceState();
method public androidx.window.WindowMetrics getMaximumWindowMetrics();
- method @Deprecated public androidx.window.WindowLayoutInfo getWindowLayoutInfo();
- 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 @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 52c9682..e8f74ed 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -33,8 +33,15 @@
public class FoldingFeature implements androidx.window.DisplayFeature {
ctor public FoldingFeature(android.graphics.Rect, int, int);
method public android.graphics.Rect getBounds();
+ method public int getOcclusionMode();
+ method public int getOrientation();
method public int getState();
- method public int getType();
+ method @Deprecated public int getType();
+ method public boolean isSeparating();
+ field public static final int OCCLUSION_FULL = 1; // 0x1
+ field public static final int OCCLUSION_NONE = 0; // 0x0
+ field public static final int ORIENTATION_HORIZONTAL = 1; // 0x1
+ field public static final int ORIENTATION_VERTICAL = 0; // 0x0
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
@@ -43,9 +50,6 @@
}
public interface WindowBackend {
- method @Deprecated public androidx.window.DeviceState getDeviceState();
- method @Deprecated public androidx.window.WindowLayoutInfo getWindowLayoutInfo(android.app.Activity);
- method @Deprecated public androidx.window.WindowLayoutInfo getWindowLayoutInfo(android.content.Context);
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!>);
method @Deprecated public void registerLayoutChangeCallback(android.content.Context, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
@@ -65,14 +69,10 @@
public final class WindowManager {
ctor public WindowManager(android.content.Context);
- ctor @Deprecated public WindowManager(android.content.Context, androidx.window.WindowBackend?);
+ ctor public WindowManager(android.content.Context, androidx.window.WindowBackend);
method public androidx.window.WindowMetrics getCurrentWindowMetrics();
- method @Deprecated public androidx.window.DeviceState getDeviceState();
method public androidx.window.WindowMetrics getMaximumWindowMetrics();
- method @Deprecated public androidx.window.WindowLayoutInfo getWindowLayoutInfo();
- 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 @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/src/androidTest/java/androidx/window/ExtensionCompatTest.java b/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java
index c0d318e..e78d762 100644
--- a/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java
+++ b/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java
@@ -17,9 +17,8 @@
package androidx.window;
import static androidx.window.ExtensionInterfaceCompat.ExtensionCallbackInterface;
-import static androidx.window.TestBoundsUtil.invalidFoldBounds;
-import static androidx.window.TestBoundsUtil.invalidHingeBounds;
-import static androidx.window.TestBoundsUtil.validFoldBound;
+import static androidx.window.TestFoldingFeatureUtil.invalidFoldBounds;
+import static androidx.window.TestFoldingFeatureUtil.validFoldBound;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -160,7 +159,6 @@
FoldingFeature foldingFeature = (FoldingFeature) capturedDisplayFeature;
assertNotNull(foldingFeature);
- assertEquals(FoldingFeature.TYPE_HINGE, foldingFeature.getType());
assertEquals(bounds, capturedDisplayFeature.getBounds());
}
@@ -272,7 +270,7 @@
ExtensionFoldingFeature.STATE_FLAT));
}
- for (Rect malformedBound : invalidHingeBounds(WINDOW_BOUNDS)) {
+ for (Rect malformedBound : invalidFoldBounds(WINDOW_BOUNDS)) {
malformedFeatures.add(new ExtensionFoldingFeature(malformedBound,
ExtensionFoldingFeature.TYPE_HINGE,
ExtensionFoldingFeature.STATE_FLAT));
diff --git a/window/window/src/androidTest/java/androidx/window/ExtensionTest.java b/window/window/src/androidTest/java/androidx/window/ExtensionTest.java
index a34be4a..f9db374 100644
--- a/window/window/src/androidTest/java/androidx/window/ExtensionTest.java
+++ b/window/window/src/androidTest/java/androidx/window/ExtensionTest.java
@@ -299,10 +299,6 @@
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();
diff --git a/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java b/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java
index 9339492..fcf70d6 100644
--- a/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java
+++ b/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java
@@ -16,23 +16,39 @@
package androidx.window;
+import static androidx.window.FoldingFeature.OCCLUSION_FULL;
+import static androidx.window.FoldingFeature.OCCLUSION_NONE;
+import static androidx.window.FoldingFeature.ORIENTATION_HORIZONTAL;
+import static androidx.window.FoldingFeature.ORIENTATION_VERTICAL;
+import static androidx.window.FoldingFeature.OcclusionType;
+import static androidx.window.FoldingFeature.Orientation;
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 androidx.window.FoldingFeature.occlusionTypeToString;
+import static androidx.window.FoldingFeature.orientationToString;
+import static androidx.window.TestFoldingFeatureUtil.allFoldStates;
+import static androidx.window.TestFoldingFeatureUtil.allFoldingFeatureTypeAndStates;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
import android.graphics.Rect;
+import androidx.annotation.NonNull;
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.List;
+
/** Tests for {@link FoldingFeature} class. */
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -44,11 +60,6 @@
}
@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);
}
@@ -68,7 +79,18 @@
new FoldingFeature(new Rect(10, 1, 10, 20), TYPE_FOLD, STATE_FLIPPED);
}
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidType() {
+ new FoldingFeature(new Rect(0, 10, 30, 10), -1, STATE_HALF_OPENED);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidState() {
+ new FoldingFeature(new Rect(0, 10, 30, 10), TYPE_FOLD, -1);
+ }
+
@Test
+ @SuppressWarnings("deprecation") // TODO(b/173739071) remove when getType is package private
public void testSetBoundsAndType() {
Rect bounds = new Rect(0, 10, 30, 10);
int type = TYPE_HINGE;
@@ -144,4 +166,116 @@
assertEquals(original, matching);
assertEquals(original.hashCode(), matching.hashCode());
}
+
+ @Test
+ public void testIsSeparating_trueForHinge() {
+ Rect bounds = new Rect(1, 0, 1, 10);
+
+ for (FoldingFeature feature : allFoldStates(bounds, TYPE_HINGE)) {
+ assertTrue(separatingModeErrorMessage(true, feature), feature.isSeparating());
+ }
+ }
+
+ @Test
+ public void testIsSeparating_falseForFlatFold() {
+ Rect bounds = new Rect(1, 0, 1, 10);
+
+ FoldingFeature feature = new FoldingFeature(bounds, TYPE_FOLD, STATE_FLAT);
+
+ assertFalse(separatingModeErrorMessage(false, feature), feature.isSeparating());
+ }
+
+ @Test
+ public void testIsSeparating_trueForNotFlatFold() {
+ Rect bounds = new Rect(1, 0, 1, 10);
+
+ List<FoldingFeature> nonFlatFeatures = new ArrayList<>();
+ for (FoldingFeature feature : allFoldStates(bounds, TYPE_FOLD)) {
+ if (feature.getState() != STATE_FLAT) {
+ nonFlatFeatures.add(feature);
+ }
+ }
+
+ for (FoldingFeature feature : nonFlatFeatures) {
+ assertTrue(separatingModeErrorMessage(true, feature), feature.isSeparating());
+ }
+ }
+
+ @Test
+ public void testOcclusionTypeNone_emptyFeature() {
+ Rect bounds = new Rect(0, 100, 100, 100);
+
+ for (FoldingFeature feature: allFoldingFeatureTypeAndStates(bounds)) {
+ assertEquals(occlusionTypeErrorMessage(OCCLUSION_NONE, feature),
+ OCCLUSION_NONE, feature.getOcclusionMode());
+ }
+ }
+
+ @Test
+ public void testOcclusionTypeFull_nonEmptyHingeFeature() {
+ Rect bounds = new Rect(0, 100, 100, 101);
+
+ for (FoldingFeature feature: allFoldStates(bounds, TYPE_HINGE)) {
+ assertEquals(occlusionTypeErrorMessage(OCCLUSION_FULL, feature),
+ OCCLUSION_FULL, feature.getOcclusionMode());
+ }
+ }
+
+ @Test
+ public void testGetFeatureOrientation_isHorizontalWhenWidthIsGreaterThanHeight() {
+ Rect bounds = new Rect(0, 100, 200, 100);
+
+ for (FoldingFeature feature: allFoldingFeatureTypeAndStates(bounds)) {
+ assertEquals(featureOrientationErrorMessage(ORIENTATION_HORIZONTAL, feature),
+ ORIENTATION_HORIZONTAL, feature.getOrientation());
+ }
+ }
+
+ @Test
+ public void testGetFeatureOrientation_isVerticalWhenHeightIsGreaterThanWidth() {
+ Rect bounds = new Rect(100, 0, 100, 200);
+
+ for (FoldingFeature feature: allFoldingFeatureTypeAndStates(bounds)) {
+ assertEquals(featureOrientationErrorMessage(ORIENTATION_VERTICAL, feature),
+ ORIENTATION_VERTICAL, feature.getOrientation());
+ }
+ }
+
+ @Test
+ public void testGetFeatureOrientation_isVerticalWhenHeightIsEqualToWidth() {
+ Rect bounds = new Rect(0, 0, 100, 100);
+
+ for (FoldingFeature feature: allFoldingFeatureTypeAndStates(bounds)) {
+ assertEquals(featureOrientationErrorMessage(ORIENTATION_VERTICAL, feature),
+ ORIENTATION_VERTICAL, feature.getOrientation());
+ }
+ }
+
+ @NonNull
+ private String separatingModeErrorMessage(boolean expected, @NonNull FoldingFeature feature) {
+ return errorMessage(FoldingFeature.class.getSimpleName(), "isSeparating",
+ Boolean.toString(expected), Boolean.toString(feature.isSeparating()), feature);
+ }
+
+ @NonNull
+ private static String occlusionTypeErrorMessage(@OcclusionType int expected,
+ FoldingFeature feature) {
+ return errorMessage(FoldingFeature.class.getSimpleName(), "getOcclusionMode",
+ occlusionTypeToString(expected),
+ occlusionTypeToString(feature.getOcclusionMode()), feature);
+ }
+
+ @NonNull
+ private static String featureOrientationErrorMessage(@Orientation int expected,
+ FoldingFeature feature) {
+ return errorMessage(FoldingFeature.class.getSimpleName(), "getFeatureOrientation",
+ orientationToString(expected),
+ orientationToString(feature.getOrientation()), feature);
+ }
+
+ private static String errorMessage(String className, String methodName, String expected,
+ String actual, Object value) {
+ return String.format("%s#%s was expected to be %s but was %s. %s: %s", className,
+ methodName, expected, actual, className, value.toString());
+ }
}
diff --git a/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java b/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java
index d2552fc..99e5f35 100644
--- a/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java
+++ b/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java
@@ -19,9 +19,8 @@
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 androidx.window.TestFoldingFeatureUtil.invalidFoldBounds;
+import static androidx.window.TestFoldingFeatureUtil.validFoldBound;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -299,7 +298,6 @@
DisplayFeature capturedDisplayFeature = capturedLayout.getDisplayFeatures().get(0);
FoldingFeature foldingFeature = (FoldingFeature) capturedDisplayFeature;
assertNotNull(foldingFeature);
- assertEquals(FoldingFeature.TYPE_HINGE, foldingFeature.getType());
assertEquals(bounds, capturedDisplayFeature.getBounds());
}
@@ -561,7 +559,7 @@
SidecarDisplayFeature.TYPE_FOLD));
}
- for (Rect malformedBound : invalidHingeBounds(WINDOW_BOUNDS)) {
+ for (Rect malformedBound : invalidFoldBounds(WINDOW_BOUNDS)) {
malformedFeatures.add(newDisplayFeature(malformedBound,
SidecarDisplayFeature.TYPE_HINGE));
}
diff --git a/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java b/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java
index 50caab8..4730074 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java
+++ b/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java
@@ -51,18 +51,14 @@
@Test
public void testFakeWindowBackend() {
WindowLayoutInfo windowLayoutInfo = newTestWindowLayout();
- DeviceState deviceState = newTestDeviceState();
- WindowBackend windowBackend = new FakeWindowBackend(windowLayoutInfo, deviceState);
+ WindowBackend windowBackend = new FakeWindowBackend(windowLayoutInfo);
TestActivity activity = mActivityTestRule.launchActivity(new Intent());
WindowManager wm = new WindowManager(activity, windowBackend);
Consumer<WindowLayoutInfo> layoutInfoConsumer = mock(Consumer.class);
- Consumer<DeviceState> stateConsumer = mock(Consumer.class);
wm.registerLayoutChangeCallback(MoreExecutors.directExecutor(), layoutInfoConsumer);
- wm.registerDeviceStateChangeCallback(MoreExecutors.directExecutor(), stateConsumer);
verify(layoutInfoConsumer).accept(windowLayoutInfo);
- verify(stateConsumer).accept(deviceState);
}
private WindowLayoutInfo newTestWindowLayout() {
@@ -74,54 +70,11 @@
return new WindowLayoutInfo(displayFeatureList);
}
- private DeviceState newTestDeviceState() {
- return new DeviceState(DeviceState.POSTURE_OPENED);
- }
-
private static class FakeWindowBackend implements WindowBackend {
private WindowLayoutInfo mWindowLayoutInfo;
- private DeviceState mDeviceState;
- private FakeWindowBackend(@NonNull WindowLayoutInfo windowLayoutInfo,
- @NonNull DeviceState deviceState) {
+ private FakeWindowBackend(@NonNull WindowLayoutInfo windowLayoutInfo) {
mWindowLayoutInfo = windowLayoutInfo;
- mDeviceState = deviceState;
- }
-
-
- /**
- * @deprecated will be removed in next alpha
- * @return nothing, throws an exception.
- */
- @Override
- @NonNull
- @Deprecated // TODO(b/173739071) Remove in next alpha.
- public DeviceState getDeviceState() {
- throw new RuntimeException("Deprecated method");
- }
-
- /**
- * @deprecated will be removed in next alpha
- * @param activity any {@link Activity}
- * @return nothing, throws an exception since this is depredcated
- */
- @Override
- @NonNull
- @Deprecated // TODO(b/173739071) Remove in next alpha.
- public WindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) {
- throw new RuntimeException("Deprecated method");
- }
-
- /**
- * @deprecated will be removed in next alpha
- * @param context any {@link Context}
- * @return nothing, throws an exception since this is deprecated.
- */
- @NonNull
- @Override
- @Deprecated // TODO(b/173739071) Remove in next alpha.
- public WindowLayoutInfo getWindowLayoutInfo(@NonNull Context context) {
- throw new RuntimeException("Deprecated method");
}
/**
@@ -152,12 +105,12 @@
@Override
public void registerDeviceStateChangeCallback(@NonNull Executor executor,
@NonNull Consumer<DeviceState> callback) {
- executor.execute(() -> callback.accept(mDeviceState));
+ throw new UnsupportedOperationException("Deprecated method");
}
@Override
public void unregisterDeviceStateChangeCallback(@NonNull Consumer<DeviceState> callback) {
- // Empty
+ throw new UnsupportedOperationException("Deprecated method");
}
}
}
diff --git a/window/window/src/androidTest/java/androidx/window/WindowManagerTest.java b/window/window/src/androidTest/java/androidx/window/WindowManagerTest.java
index 83782d0..8b8e142 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowManagerTest.java
+++ b/window/window/src/androidTest/java/androidx/window/WindowManagerTest.java
@@ -86,21 +86,6 @@
}
@Test
- public void testRegisterDeviceStateChangeCallback() {
- WindowBackend backend = mock(WindowBackend.class);
- Activity activity = mock(Activity.class);
- WindowManager wm = new WindowManager(activity, backend);
-
- Executor executor = MoreExecutors.directExecutor();
- Consumer<DeviceState> consumer = mock(Consumer.class);
- wm.registerDeviceStateChangeCallback(executor, consumer);
- verify(backend).registerDeviceStateChangeCallback(executor, consumer);
-
- wm.unregisterDeviceStateChangeCallback(consumer);
- verify(backend).unregisterDeviceStateChangeCallback(eq(consumer));
- }
-
- @Test
public void testGetCurrentWindowMetrics() {
WindowBackend backend = mock(WindowBackend.class);
Activity activity = mock(Activity.class);
diff --git a/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java b/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java
index 3379725..692e3c7 100644
--- a/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java
+++ b/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java
@@ -32,7 +32,6 @@
import androidx.window.extensions.ExtensionInterface;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
@@ -96,56 +95,6 @@
return sInstance;
}
- /**
- * @deprecated will be removed in the next alpha.
- * @return {@link DeviceState} when Sidecar is present and an unknown {@link DeviceState}
- * otherwise.
- */
- @Override
- @NonNull
- @Deprecated
- public DeviceState getDeviceState() {
- synchronized (sLock) {
- if (mWindowExtension instanceof SidecarCompat) {
- SidecarCompat sidecarCompat = (SidecarCompat) mWindowExtension;
- return sidecarCompat.getDeviceState();
- }
- return new DeviceState(DeviceState.POSTURE_UNKNOWN);
- }
- }
-
- /**
- * @deprecated will be removed in the next alpha.
- * @param activity that is running.
- * @return {@link WindowLayoutInfo} for the window containing the {@link Activity} when
- * Sidecar is present and an empty info otherwise
- */
- @NonNull
- @Override
- @Deprecated
- public WindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) {
- synchronized (sLock) {
- if (mWindowExtension instanceof SidecarCompat) {
- SidecarCompat sidecarCompat = (SidecarCompat) mWindowExtension;
- return sidecarCompat.getWindowLayoutInfo(activity);
- }
- return new WindowLayoutInfo(Collections.emptyList());
- }
- }
-
- /**
- *
- * @param context with an associated {@link Activity}
- * @return the {@link WindowLayoutInfo}
- * @deprecated use an {@link Activity} instead of {@link Context}
- */
- @NonNull
- @Override
- @Deprecated
- public WindowLayoutInfo getWindowLayoutInfo(@NonNull Context context) {
- return getWindowLayoutInfo(assertActivityContext(context));
- }
-
@Override
public void registerLayoutChangeCallback(@NonNull Context context, @NonNull Executor executor,
@NonNull Consumer<WindowLayoutInfo> callback) {
diff --git a/window/window/src/main/java/androidx/window/FoldingFeature.java b/window/window/src/main/java/androidx/window/FoldingFeature.java
index 34f2b68..388cff7 100644
--- a/window/window/src/main/java/androidx/window/FoldingFeature.java
+++ b/window/window/src/main/java/androidx/window/FoldingFeature.java
@@ -72,6 +72,46 @@
*/
public static final int STATE_FLIPPED = 3;
+ /**
+ * The {@link FoldingFeature} does not occlude the content in any way. One example is a flat
+ * continuous fold where content can stretch across the fold. Another example is a hinge that
+ * has width or height equal to 0. In this case the content is physically split across both
+ * displays, but fully visible.
+ */
+ public static final int OCCLUSION_NONE = 0;
+
+ /**
+ * The {@link FoldingFeature} occludes all content. One example is a hinge that is considered to
+ * be part of the window, so that part of the UI is not visible to the user. Any content shown
+ * in the same area as the hinge may not be accessible in any way. Fully occluded areas should
+ * always be avoided when placing interactive UI elements and text.
+ */
+ public static final int OCCLUSION_FULL = 1;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ OCCLUSION_NONE,
+ OCCLUSION_FULL
+ })
+ @interface OcclusionType {}
+
+ /**
+ * The height of the {@link FoldingFeature} is greater than or equal to the width.
+ */
+ public static final int ORIENTATION_VERTICAL = 0;
+
+ /**
+ * The width of the {@link FoldingFeature} is greater than the height.
+ */
+ public static final int ORIENTATION_HORIZONTAL = 1;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ ORIENTATION_HORIZONTAL,
+ ORIENTATION_VERTICAL
+ })
+ @interface Orientation {}
+
@Retention(RetentionPolicy.SOURCE)
@IntDef({
STATE_HALF_OPENED,
@@ -100,7 +140,9 @@
private final int mState;
public FoldingFeature(@NonNull Rect bounds, @Type int type, @State int state) {
- validateFeatureBounds(bounds, type);
+ validateState(state);
+ validateType(type);
+ validateFeatureBounds(bounds);
mBounds = new Rect(bounds);
mType = type;
mState = state;
@@ -112,7 +154,13 @@
return new Rect(mBounds);
}
+ /**
+ * Returns type that is either {@link FoldingFeature#TYPE_FOLD} or
+ * {@link FoldingFeature#TYPE_HINGE}
+ * @deprecated visibility will be reduced.
+ */
@Type
+ @Deprecated
public int getType() {
return mType;
}
@@ -123,28 +171,127 @@
}
/**
+ * Calculates if a {@link FoldingFeature} should be thought of as splitting the window into
+ * multiple physical areas that can be seen by users as logically separate. Display panels
+ * connected by a hinge are always separated. Folds on flexible screens should be treated as
+ * separating when they are not {@link FoldingFeature#STATE_FLAT}.
+ *
+ * Apps may use this to determine if content should lay out around the {@link FoldingFeature}.
+ * Developers should consider the placement of interactive elements. Similar to the case of
+ * {@link FoldingFeature#OCCLUSION_FULL}, when a feature is separating then consider laying
+ * out the controls around the {@link FoldingFeature}.
+ *
+ * An example use case is to determine if the UI should be split into two logical areas. A media
+ * app where there is some auxiliary content, such as comments or description of a video, may
+ * need to adapt the layout. The media can be put on one side of the {@link FoldingFeature} and
+ * the auxiliary content can be placed on the other side.
+ *
+ * @return {@code true} if the feature splits the display into two areas, {@code false}
+ * otherwise.
+ */
+ public boolean isSeparating() {
+ if (mType == TYPE_HINGE) {
+ return true;
+ }
+ if (mType == TYPE_FOLD && (mState == STATE_FLIPPED || mState == STATE_HALF_OPENED)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Calculates the occlusion mode to determine if a {@link FoldingFeature} occludes a part of
+ * the window. This flag is useful for determining if UI elements need to be moved
+ * around so that the user can access them. For some devices occluded elements can not be
+ * accessed by the user at all.
+ *
+ * For occlusion type {@link FoldingFeature#OCCLUSION_NONE} the feature can be treated as a
+ * guideline. One example would be for a continuously folding screen. For occlusion type
+ * {@link FoldingFeature#OCCLUSION_FULL} the feature should be avoided completely since content
+ * will not be visible or touchable, like a hinge device with two displays.
+ *
+ * The occlusion mode is useful to determine if the UI needs to adapt to the
+ * {@link FoldingFeature}. For example, full screen games should consider avoiding anything in
+ * the occluded region if it negatively affects the gameplay. The user can not tap
+ * on the occluded interactive UI elements nor can they see important information.
+ *
+ * @return {@link FoldingFeature#OCCLUSION_NONE} if the {@link FoldingFeature} has empty
+ * bounds.
+ */
+ @OcclusionType
+ public int getOcclusionMode() {
+ if (mBounds.width() == 0 || mBounds.height() == 0) {
+ return OCCLUSION_NONE;
+ }
+ return OCCLUSION_FULL;
+ }
+
+ /**
+ * Returns {@link FoldingFeature#ORIENTATION_HORIZONTAL} if the width is greater than the
+ * height, {@link FoldingFeature#ORIENTATION_VERTICAL} otherwise.
+ */
+ @Orientation
+ public int getOrientation() {
+ return mBounds.width() > mBounds.height()
+ ? ORIENTATION_HORIZONTAL
+ : ORIENTATION_VERTICAL;
+ }
+
+ static String occlusionTypeToString(@OcclusionType int type) {
+ switch (type) {
+ case OCCLUSION_NONE:
+ return "OCCLUSION_NONE";
+ case OCCLUSION_FULL:
+ return "OCCLUSION_FULL";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ static String orientationToString(@Orientation int direction) {
+ switch (direction) {
+ case ORIENTATION_HORIZONTAL:
+ return "ORIENTATION_HORIZONTAL";
+ case ORIENTATION_VERTICAL:
+ return "ORIENTATION_VERTICAL";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ /**
+ * Verifies the state is {@link FoldingFeature#STATE_FLAT},
+ * {@link FoldingFeature#STATE_HALF_OPENED} or {@link FoldingFeature#STATE_FLIPPED}.
+ */
+ private static void validateState(int state) {
+ if (state != STATE_FLAT && state != STATE_HALF_OPENED && state != STATE_FLIPPED) {
+ throw new IllegalArgumentException("State must be either " + stateToString(STATE_FLAT)
+ + ", " + stateToString(STATE_HALF_OPENED) + ", or "
+ + stateToString(STATE_FLIPPED));
+ }
+ }
+
+ /**
+ * Verifies the type is either {@link FoldingFeature#TYPE_HINGE} or
+ * {@link FoldingFeature#TYPE_FOLD}
+ */
+ private static void validateType(int type) {
+ if (type != TYPE_FOLD && type != TYPE_HINGE) {
+ throw new IllegalArgumentException("Type must be either " + typeToString(TYPE_FOLD)
+ + " or " + typeToString(TYPE_HINGE));
+ }
+ }
+
+ /**
* Verifies the bounds of the folding feature.
*/
- private static void validateFeatureBounds(@NonNull Rect bounds, int type) {
+ private static void validateFeatureBounds(@NonNull Rect bounds) {
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));
- }
+ if (bounds.left != 0 && bounds.top != 0) {
+ throw new IllegalArgumentException("Bounding rectangle must start at the top or "
+ + "left window edge for folding features");
}
}
@@ -178,7 +325,7 @@
@Override
public String toString() {
return FoldingFeature.class.getSimpleName() + " { " + mBounds + ", type="
- + typeToString(getType()) + ", state=" + stateToString(mState) + " }";
+ + typeToString(mType) + ", state=" + stateToString(mState) + " }";
}
@Override
diff --git a/window/window/src/main/java/androidx/window/SidecarCompat.java b/window/window/src/main/java/androidx/window/SidecarCompat.java
index 52c686c..3659a84 100644
--- a/window/window/src/main/java/androidx/window/SidecarCompat.java
+++ b/window/window/src/main/java/androidx/window/SidecarCompat.java
@@ -69,10 +69,6 @@
}
}
- DeviceState getDeviceState() {
- return mSidecarAdapter.translate(mSidecar.getDeviceState());
- }
-
@VisibleForTesting
SidecarCompat(@NonNull SidecarInterface sidecar, SidecarAdapter sidecarAdapter) {
// Empty implementation to avoid null checks.
diff --git a/window/window/src/main/java/androidx/window/WindowBackend.java b/window/window/src/main/java/androidx/window/WindowBackend.java
index 1a874f3..b45817b 100644
--- a/window/window/src/main/java/androidx/window/WindowBackend.java
+++ b/window/window/src/main/java/androidx/window/WindowBackend.java
@@ -31,33 +31,6 @@
public interface WindowBackend {
/**
- * @return the {@link DeviceState} when the Sidecar library is present and a {@link DeviceState}
- * with {@code POSTURE_UNKNOWN} otherwise.
- * @deprecated will be removed in the next alpha
- */
- @Deprecated
- @NonNull
- DeviceState getDeviceState();
-
- /**
- * @return the {@link WindowLayoutInfo} when Sidecar library is present and an empty info
- * otherwise
- * @deprecated will be removed in the next alpha
- */
- @Deprecated
- @NonNull
- WindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity);
-
- /**
- * @return the {@link WindowLayoutInfo} when Sidecar library is present and an empty info
- * otherwise
- * @deprecated will be removed in the next alpha
- */
- @Deprecated
- @NonNull
- WindowLayoutInfo getWindowLayoutInfo(@NonNull Context context);
-
- /**
* Registers a callback for layout changes of the window for the supplied {@link Activity}.
* Must be called only after the it is attached to the window.
*/
diff --git a/window/window/src/main/java/androidx/window/WindowManager.java b/window/window/src/main/java/androidx/window/WindowManager.java
index c36bb46..df22d0f 100644
--- a/window/window/src/main/java/androidx/window/WindowManager.java
+++ b/window/window/src/main/java/androidx/window/WindowManager.java
@@ -40,11 +40,11 @@
* from the {@link androidx.window.sidecar.SidecarInterface} or is passed directly to the
* {@link ExtensionInterface}.
*/
- private Activity mActivity;
+ private final Activity mActivity;
/**
* The backend that supplies the information through this class.
*/
- private WindowBackend mWindowBackend;
+ private final WindowBackend mWindowBackend;
/**
* Gets an instance of the class initialized with and connected to the provided {@link Context}.
@@ -68,10 +68,8 @@
* 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.
- * @deprecated WindowBackend will be a required argument in the next implementation.
*/
- @Deprecated
- public WindowManager(@NonNull Context context, @Nullable WindowBackend windowBackend) {
+ public WindowManager(@NonNull Context context, @NonNull WindowBackend windowBackend) {
Activity activity = getActivityFromContext(context);
if (activity == null) {
throw new IllegalArgumentException("Used non-visual Context to obtain an instance of "
@@ -102,46 +100,6 @@
}
/**
- * @deprecated will be removed in the next alpha
- * @return the current {@link DeviceState} if Sidecar is present and an empty info otherwise
- */
- @Deprecated
- @NonNull
- public DeviceState getDeviceState() {
- return mWindowBackend.getDeviceState();
- }
-
- /**
- * @deprecated will be removed in the next alpha
- * @return the current {@link WindowLayoutInfo} when Sidecar is present and an empty info
- * otherwise
- */
- @Deprecated
- @NonNull
- public WindowLayoutInfo getWindowLayoutInfo() {
- return mWindowBackend.getWindowLayoutInfo(mActivity);
- }
-
- /**
- * @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);
- }
-
- /**
* Returns the {@link WindowMetrics} according to the current system state.
* <p>
* The metrics describe the size of the area the window would occupy with
diff --git a/window/window/src/test/java/androidx/window/SidecarCompatUnitTest.java b/window/window/src/test/java/androidx/window/SidecarCompatUnitTest.java
index 752b99d..63183ac 100644
--- a/window/window/src/test/java/androidx/window/SidecarCompatUnitTest.java
+++ b/window/window/src/test/java/androidx/window/SidecarCompatUnitTest.java
@@ -17,9 +17,8 @@
package androidx.window;
import static androidx.window.ActivityUtil.getActivityWindowToken;
-import static androidx.window.TestBoundsUtil.invalidFoldBounds;
-import static androidx.window.TestBoundsUtil.invalidHingeBounds;
-import static androidx.window.TestBoundsUtil.validFoldBound;
+import static androidx.window.TestFoldingFeatureUtil.invalidFoldBounds;
+import static androidx.window.TestFoldingFeatureUtil.validFoldBound;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
@@ -373,7 +372,7 @@
SidecarDisplayFeature.TYPE_FOLD));
}
- for (Rect malformedBound : invalidHingeBounds(WINDOW_BOUNDS)) {
+ for (Rect malformedBound : invalidFoldBounds(WINDOW_BOUNDS)) {
malformedFeatures.add(newDisplayFeature(malformedBound,
SidecarDisplayFeature.TYPE_HINGE));
}
diff --git a/window/window/src/testUtil/java/androidx/window/TestBoundsUtil.java b/window/window/src/testUtil/java/androidx/window/TestBoundsUtil.java
deleted file mode 100644
index 3f5ab44..0000000
--- a/window/window/src/testUtil/java/androidx/window/TestBoundsUtil.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.window;
-
-import android.graphics.Rect;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A class containing static methods for creating different window bound types. Test methods are
- * shared between the unit tests and the instrumentation tests.
- */
-final class TestBoundsUtil {
-
- private TestBoundsUtil() { }
-
- /**
- * @param windowBounds the bounds for a window contain a valid fold.
- * @return {@link Rect} that is a valid fold bound within the given window.
- */
- public static Rect validFoldBound(Rect windowBounds) {
- int verticalMid = windowBounds.height() / 2;
- return new Rect(0, verticalMid, windowBounds.width(), verticalMid);
- }
-
- /**
- * @return {@link Rect} containing the invalid zero bounds.
- */
- static Rect invalidZeroBound() {
- return new Rect();
- }
-
- /**
- * @param windowBounds the bounds for a window contain an invalid fold.
- * @return {@link Rect} for bounds where the width is shorter than the window width.
- */
- static Rect invalidBoundShortWidth(Rect windowBounds) {
- return new Rect(0, 0, windowBounds.width() / 2, 0);
- }
-
- /**
- * @param windowBounds the bounds for a window contain an invalid fold.
- * @return {@link Rect} for bounds where the height is shorter than the window height.
- */
- static Rect invalidBoundShortHeight(Rect windowBounds) {
- return new Rect(0, 0, 0, windowBounds.height() / 2);
- }
-
- /**
- * @param windowBounds the bounds for a window contain an invalid fold.
- * @return a {@link List} of {@link Rect} of invalid bounds for fold features
- */
- static List<Rect> invalidFoldBounds(Rect windowBounds) {
- List<Rect> badBounds = invalidHingeBounds(windowBounds);
- Rect nonEmptySmallRect = new Rect(0, 0, 1, 1);
- badBounds.add(nonEmptySmallRect);
- return badBounds;
- }
-
- /**
- * @param windowBounds the bounds for a window contain an invalid fold.
- * @return a {@link List} of {@link Rect} of invalid bounds for hinge features
- */
- static List<Rect> invalidHingeBounds(Rect windowBounds) {
- List<Rect> badBounds = new ArrayList<>();
-
- badBounds.add(invalidZeroBound());
- badBounds.add(invalidBoundShortWidth(windowBounds));
- badBounds.add(invalidBoundShortHeight(windowBounds));
-
- return badBounds;
- }
-}
diff --git a/window/window/src/testUtil/java/androidx/window/TestFoldingFeatureUtil.java b/window/window/src/testUtil/java/androidx/window/TestFoldingFeatureUtil.java
new file mode 100644
index 0000000..bc14ba6
--- /dev/null
+++ b/window/window/src/testUtil/java/androidx/window/TestFoldingFeatureUtil.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window;
+
+import 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 android.graphics.Rect;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class containing static methods for creating different window bound types. Test methods are
+ * shared between the unit tests and the instrumentation tests.
+ */
+final class TestFoldingFeatureUtil {
+
+ private TestFoldingFeatureUtil() {
+ }
+
+ /**
+ * @param windowBounds the bounds of the window.
+ * @return {@link Rect} that is a valid fold bound within the given window.
+ */
+ static Rect validFoldBound(Rect windowBounds) {
+ int verticalMid = windowBounds.height() / 2;
+ return new Rect(0, verticalMid, windowBounds.width(), verticalMid);
+ }
+
+ /**
+ * @return {@link Rect} containing the invalid zero bounds.
+ */
+ static Rect invalidZeroBound() {
+ return new Rect();
+ }
+
+ /**
+ * @param windowBounds the bounds of the window.
+ * @return {@link Rect} for bounds where the width is shorter than the window width.
+ */
+ static Rect invalidBoundShortWidth(Rect windowBounds) {
+ return new Rect(0, 0, windowBounds.width() / 2, 0);
+ }
+
+ /**
+ * @param windowBounds the bounds of the window.
+ * @return {@link Rect} for bounds where the height is shorter than the window height.
+ */
+ static Rect invalidBoundShortHeight(Rect windowBounds) {
+ return new Rect(0, 0, 0, windowBounds.height() / 2);
+ }
+
+ /**
+ * @param windowBounds the bounds of the window.
+ * @return a {@link List} of {@link Rect} of invalid bounds for folding features
+ */
+ static List<Rect> invalidFoldBounds(Rect windowBounds) {
+ List<Rect> badBounds = new ArrayList<>();
+
+ badBounds.add(invalidZeroBound());
+ badBounds.add(invalidBoundShortWidth(windowBounds));
+ badBounds.add(invalidBoundShortHeight(windowBounds));
+
+ return badBounds;
+ }
+
+ /**
+ * @param bounds for the test {@link FoldingFeature}
+ * @param type of the {@link FoldingFeature}
+ * @return {@link List} of {@link FoldingFeature} containing all the possible states for the
+ * given type.
+ */
+ static List<FoldingFeature> allFoldStates(Rect bounds, @FoldingFeature.Type int type) {
+ List<FoldingFeature> states = new ArrayList<>();
+
+ states.add(new FoldingFeature(bounds, type, STATE_FLAT));
+ states.add(new FoldingFeature(bounds, type, STATE_HALF_OPENED));
+ states.add(new FoldingFeature(bounds, type, STATE_FLIPPED));
+
+ return states;
+ }
+
+ /**
+ * @param bounds for the test {@link FoldingFeature}
+ * @return {@link List} of {@link FoldingFeature} containing all the possible states and
+ * types.
+ */
+ static List<FoldingFeature> allFoldingFeatureTypeAndStates(Rect bounds) {
+ List<FoldingFeature> features = new ArrayList<>();
+ features.addAll(allFoldStates(bounds, TYPE_HINGE));
+ features.addAll(allFoldStates(bounds, TYPE_FOLD));
+ return features;
+ }
+}